mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45229b1af7 | ||
|
|
f9a3fb2f1b | ||
|
|
65d8a707d8 | ||
|
|
cc6bf45d5f | ||
|
|
8fbb777665 | ||
|
|
0e041d460c | ||
|
|
2e3b296021 | ||
|
|
405b50edcd | ||
|
|
fff540010e | ||
|
|
e513694912 | ||
|
|
d01cada16c | ||
|
|
dd4fecfd1c | ||
|
|
095d7c6bcb | ||
|
|
d165a04377 | ||
|
|
d3d1e47950 | ||
|
|
9c14941c15 | ||
|
|
1627f65bd7 | ||
|
|
19f4f3c1a5 | ||
|
|
ae70680ceb | ||
|
|
60fc13c765 | ||
|
|
60c3d41c8e | ||
|
|
fb8ff37d83 | ||
|
|
0d9b30e730 | ||
|
|
695f42df80 | ||
|
|
6b43159be2 | ||
|
|
21c9c8b4fb | ||
|
|
c4abe54c3f | ||
|
|
dd71c9e71b | ||
|
|
2be3e4bf69 | ||
|
|
f34e9f7b26 | ||
|
|
76b0729af3 | ||
|
|
4877bc3849 | ||
|
|
0742e3415c | ||
|
|
ae7e3a722c | ||
|
|
7f2e19250f | ||
|
|
4e16e954ef | ||
|
|
b6c3205474 | ||
|
|
23076b41c6 | ||
|
|
b5116b54af | ||
|
|
83aaa21b5b | ||
|
|
e1e7b37ce5 | ||
|
|
8dab9268f2 | ||
|
|
4eed999db1 | ||
|
|
c29ab50a3d | ||
|
|
5e1d6cba4a | ||
|
|
a645d1459c | ||
|
|
24e11a864c | ||
|
|
87a4778a91 | ||
|
|
0750af4c68 | ||
|
|
60e613fac8 | ||
|
|
b75baf57ba | ||
|
|
137df3c5c0 | ||
|
|
6ef2daebbd | ||
|
|
55f85e3728 | ||
|
|
f0269069d2 | ||
|
|
61dbca3243 | ||
|
|
f21cb240c4 | ||
|
|
ca46e14732 | ||
|
|
87f6000b85 | ||
|
|
36d0550472 | ||
|
|
ee4734c957 | ||
|
|
02f9fc0a7b | ||
|
|
6ce657d891 |
19
.github/workflows/unit-tests.yml
vendored
Normal file
19
.github/workflows/unit-tests.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-lang
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
@@ -1,16 +0,0 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: "bruno"
|
||||
links:
|
||||
- url: https://example.com/user
|
||||
title: Examples Users
|
||||
icon: user
|
||||
- url: https://example.com/group
|
||||
title: Example Group
|
||||
icon: group
|
||||
spec:
|
||||
type: component
|
||||
lifecycle: production
|
||||
owner: anoop
|
||||
system: tech-docs
|
||||
@@ -1,6 +1,16 @@
|
||||
## development
|
||||
|
||||
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
###
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i
|
||||
|
||||
@@ -14,14 +24,17 @@ npm run dev --workspace=packages/bruno-electron
|
||||
npm run build --workspace=packages/bruno-app
|
||||
```
|
||||
|
||||
## fix
|
||||
### fix
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
|
||||
# testing
|
||||
### testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
```
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-tauri",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-testbench",
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
publicRuntimeConfig: {
|
||||
CI: process.env.CI,
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -15,8 +16,8 @@
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -34,16 +35,18 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "12.3.1",
|
||||
"next": "12.3.3",
|
||||
"path": "^0.12.7",
|
||||
"platform": "^1.3.6",
|
||||
"posthog-node": "^2.1.0",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tabs": "^3.2.3",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"reckonjs": "^0.1.2",
|
||||
"sass": "^1.46.0",
|
||||
"split-on-first": "^3.0.0",
|
||||
|
||||
@@ -27,6 +27,9 @@ const StyledWrapper = styled.div`
|
||||
.cm-s-monokai span.cm-atom{
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid{color: green}
|
||||
.cm-variable-invalid{color: red}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
@@ -15,7 +18,7 @@ if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export default class QueryEditor extends React.Component {
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -23,6 +26,7 @@ export default class QueryEditor extends React.Component {
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -70,6 +74,7 @@ export default class QueryEditor extends React.Component {
|
||||
}));
|
||||
if (editor) {
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +93,13 @@ export default class QueryEditor extends React.Component {
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setOption('mode', this.props.mode);
|
||||
}
|
||||
|
||||
if(this.editor) {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
@@ -116,6 +127,15 @@ export default class QueryEditor extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
const mode = this.props.mode || 'application/ld+json';
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-enviroment {
|
||||
background-color: ${(props) => props.theme.sidebar.workspace.bg};
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 15px;
|
||||
|
||||
.caret {
|
||||
|
||||
@@ -69,7 +69,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Settings</span>
|
||||
<span>Configure</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
const EnvironmentDetails = ({ environment, collection }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
console.log(environment);
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
|
||||
@@ -4,6 +4,8 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
@@ -19,6 +21,8 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
@@ -30,10 +34,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +65,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +86,16 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -67,6 +67,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
switch (tab) {
|
||||
case 'query': {
|
||||
return <QueryEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
schema={schema}
|
||||
width={leftPaneWidth}
|
||||
|
||||
@@ -4,6 +4,8 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MultipartFormParams = ({ item, collection }) => {
|
||||
@@ -19,6 +21,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
@@ -30,10 +34,6 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +65,6 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +86,16 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
<SingleLineEditor
|
||||
onSave={onSave}
|
||||
value={param.value}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -29,6 +29,9 @@ const StyledWrapper = styled.div`
|
||||
.cm-s-monokai span.cm-atom{
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid{color: green}
|
||||
.cm-variable-invalid{color: red}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import MD from 'markdown-it';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
import onHasCompletion from './onHasCompletion';
|
||||
@@ -29,6 +32,7 @@ export default class QueryEditor extends React.Component {
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -36,7 +40,10 @@ export default class QueryEditor extends React.Component {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
tabSize: 2,
|
||||
mode: 'graphql',
|
||||
mode: 'brunovariables',
|
||||
brunoVarInfo: {
|
||||
variables: getEnvironmentVariables(this.props.collection),
|
||||
},
|
||||
theme: this.props.editorTheme || 'graphiql',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
keyMap: 'sublime',
|
||||
@@ -129,6 +136,7 @@ export default class QueryEditor extends React.Component {
|
||||
editor.on('hasCompletion', this._onHasCompletion);
|
||||
editor.on('beforeChange', this._onBeforeChange);
|
||||
}
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -152,6 +160,11 @@ export default class QueryEditor extends React.Component {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -163,6 +176,14 @@ export default class QueryEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'graphql');
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper
|
||||
|
||||
@@ -4,6 +4,8 @@ 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 SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -20,6 +22,8 @@ const QueryParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
|
||||
@@ -32,10 +36,6 @@ const QueryParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -68,7 +68,6 @@ const QueryParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -90,27 +89,16 @@ const QueryParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -2,9 +2,11 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SendIcon from 'components/Icons/Send';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
@@ -13,6 +15,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
@@ -39,15 +42,12 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<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"
|
||||
value={url}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={(event) => onUrlChange(event.target.value)}
|
||||
<SingleLineEditor
|
||||
value={url}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<SendIcon color={theme.requestTabPanel.url.icon} width={22}/>
|
||||
|
||||
@@ -45,7 +45,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
|
||||
<CodeEditor collection={collection} theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
@@ -19,6 +21,8 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
@@ -30,10 +34,6 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
header.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
header.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
header.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +65,6 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +86,16 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={header.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleHeaderValueChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, header, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const RequestNotFound = ({ itemUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
const closeTab = () => {
|
||||
dispatch(
|
||||
@@ -13,11 +14,25 @@ const RequestNotFound = ({ itemUid }) => {
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
// add a delay component in react that shows a loading spinner
|
||||
// and then shows the error message after a delay
|
||||
// this will prevent the error message from flashing on the screen
|
||||
|
||||
if(!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 className="mt-2">This can happen when the .bru 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
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
|
||||
const RequestTabNotFound = ({handleCloseClick}) => {
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
// add a delay component in react that shows a loading spinner
|
||||
// and then shows the error message after a delay
|
||||
// this will prevent the error message from flashing on the screen
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
if(!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">
|
||||
{showErrorMessage ? (
|
||||
<>
|
||||
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Not Found</span>
|
||||
</>
|
||||
) : null}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestTabNotFound;
|
||||
@@ -4,7 +4,7 @@ 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';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -61,18 +61,7 @@ const RequestTab = ({ tab, collection }) => {
|
||||
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>
|
||||
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ const NetworkError = ({ onClose }) => {
|
||||
<div className="flex items-start">
|
||||
<div className="ml-3 flex-1">
|
||||
<p className="text-sm font-medium text-red-800">Network Error</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Please note that if you are using Bruno on the web, then the api you are connecting to must allow CORS. If not, please use the chrome extension or the desktop app
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import React from 'react';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryResult = ({ value, width }) => {
|
||||
const QueryResult = ({ item, collection, value, width }) => {
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
||||
<div className="h-full">
|
||||
<CodeEditor theme={storedTheme} value={value || ''} readOnly />
|
||||
<CodeEditor collection={collection} theme={storedTheme} onRun={onRun} value={value || ''} readOnly />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: Inter, sans-serif !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.request {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.response {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Timeline = ({ item }) => {
|
||||
const request = item.requestSent || {};
|
||||
const response = item.response || {};
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = response.headers || [];
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = safeStringifyJSON(request.data);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 pb-4 w-full">
|
||||
<div>
|
||||
<pre className='line request font-bold'>
|
||||
<span className="arrow">{'>'}</span> {request.method} {request.url}
|
||||
</pre>
|
||||
{requestHeaders.map((h) => {
|
||||
return (
|
||||
<pre className='line request' key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
|
||||
{requestData ? (
|
||||
<pre className='line request'>
|
||||
<span className="arrow">{'>'}</span> data {requestData}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className='mt-4'>
|
||||
<pre className='line response font-bold'>
|
||||
<span className="arrow">{'<'}</span> {response.status} {response.statusText}
|
||||
</pre>
|
||||
|
||||
{responseHeaders.map((h) => {
|
||||
return (
|
||||
<pre className='line response' key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
@@ -10,6 +10,7 @@ import ResponseHeaders from './ResponseHeaders';
|
||||
import StatusCode from './StatusCode';
|
||||
import ResponseTime from './ResponseTime';
|
||||
import ResponseSize from './ResponseSize';
|
||||
import Timeline from './Timeline';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
@@ -32,11 +33,19 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <QueryResult width={rightPaneWidth} value={response.data ? JSON.stringify(response.data, null, 2) : ''} />;
|
||||
return <QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
value={response.data ? JSON.stringify(response.data, null, 2) : ''
|
||||
} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline item={item} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
@@ -84,6 +93,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={response.status} />
|
||||
|
||||
@@ -70,7 +70,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging .collection-item-name {
|
||||
&.is-sidebar-dragging .collection-item-name {
|
||||
cursor: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { useState, useRef, forwardRef, useEffect } from 'react';
|
||||
import range from 'lodash/range';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -23,7 +25,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isDragging = useSelector((state) => state.app.isDragging);
|
||||
const isSidebarDragging = useSelector((state) => state.app.isDragging);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
@@ -33,6 +35,29 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'COLLECTION_ITEM',
|
||||
item: item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
})
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: 'COLLECTION_ITEM',
|
||||
drop: (draggedItem) => {
|
||||
if (draggedItem.uid !== item.uid) {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
}
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
return draggedItem.uid !== item.uid;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText && searchText.length) {
|
||||
setItemisCollapsed(false);
|
||||
@@ -91,7 +116,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const isFolder = isItemAFolder(item);
|
||||
|
||||
const className = classnames('flex flex-col w-full', {
|
||||
'is-dragging': isDragging
|
||||
'is-sidebar-dragging': isSidebarDragging
|
||||
});
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
@@ -106,8 +131,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const requestItems = filter(item.items, (i) => isItemARequest(i));
|
||||
const folderItems = filter(item.items, (i) => isItemAFolder(i));
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
@@ -116,7 +151,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{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}>
|
||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
? indents.map((i) => {
|
||||
@@ -214,13 +249,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
|
||||
{!itemIsCollapsed ? (
|
||||
<div>
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
button.submit {
|
||||
color: white;
|
||||
background-color: var(--color-background-danger) !important;
|
||||
border: inherit !important;
|
||||
|
||||
&:hover {
|
||||
border: inherit !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { deleteCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteCollection = ({ onClose, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteCollection(collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection deleted');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while deleting the collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title="Delete Collection" confirmText="Delete" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
Are you sure you want to delete the collection <span className="font-semibold">{collection.name}</span> ?
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteCollection;
|
||||
@@ -2,13 +2,13 @@ import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { removeLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { removeCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const RemoveLocalCollection = ({ onClose, collection }) => {
|
||||
const RemoveCollection = ({ onClose, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onConfirm = () => {
|
||||
dispatch(removeLocalCollection(collection.uid))
|
||||
dispatch(removeCollection(collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection removed');
|
||||
onClose();
|
||||
@@ -23,4 +23,4 @@ const RemoveLocalCollection = ({ onClose, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveLocalCollection;
|
||||
export default RemoveCollection;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { recursivelyGetAllItemUids } from 'utils/collections';
|
||||
import { removeCollectionFromWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { removeLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
const RemoveCollectionFromWorkspace = ({ onClose, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
|
||||
const onConfirm = () => {
|
||||
dispatch(removeCollectionFromWorkspace(activeWorkspaceUid, collection.uid))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: recursivelyGetAllItemUids(collection.items)
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => toast.success('Collection removed from workspace'))
|
||||
.catch((err) => console.log(err) && toast.error('An error occured while removing the collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Remove Collection from Workspace" confirmText="Remove" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
Are you sure you want to remove the collection <span className="font-semibold">{collection.name}</span> from this workspace?
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveCollectionFromWorkspace;
|
||||
@@ -2,30 +2,28 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import filter from 'lodash/filter';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
import CollectionItem from './CollectionItem';
|
||||
import RemoveCollectionFromWorkspace from './RemoveCollectionFromWorkspace';
|
||||
import RemoveLocalCollection from './RemoveLocalCollection';
|
||||
import RemoveCollection from './RemoveCollection';
|
||||
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb, isLocalCollection } from 'utils/collections';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
|
||||
import exportCollection from 'utils/collections/export';
|
||||
|
||||
import RenameCollection from './RenameCollection';
|
||||
import DeleteCollection from './DeleteCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionFromWSModal, setShowRemoveCollectionFromWSModal] = useState(false);
|
||||
const [showRemoveLocalCollectionModal, setShowRemoveLocalCollectionModal] = useState(false);
|
||||
const [showDeleteCollectionModal, setShowDeleteCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -61,25 +59,45 @@ const Collection = ({ collection, searchText }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const requestItems = filter(collection.items, (i) => isItemARequest(i));
|
||||
const folderItems = filter(collection.items, (i) => isItemAFolder(i));
|
||||
|
||||
const handleExportClick = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
||||
};
|
||||
|
||||
const isLocal = isLocalCollection(collection);
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: 'COLLECTION_ITEM',
|
||||
drop: (draggedItem) => {
|
||||
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
// todo need to make sure that draggedItem belongs to the collection
|
||||
return true;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(collection.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(collection.items, (i) => isItemAFolder(i)));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col">
|
||||
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />}
|
||||
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)} />}
|
||||
{showRenameCollectionModal && <RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />}
|
||||
{showRemoveCollectionFromWSModal && <RemoveCollectionFromWorkspace collection={collection} onClose={() => setShowRemoveCollectionFromWSModal(false)} />}
|
||||
{showDeleteCollectionModal && <DeleteCollection collection={collection} onClose={() => setShowDeleteCollectionModal(false)} />}
|
||||
{showRemoveLocalCollectionModal && <RemoveLocalCollection collection={collection} onClose={() => setShowRemoveLocalCollectionModal(false)} />}
|
||||
<div className="flex py-1 collection-name items-center">
|
||||
{showRemoveCollectionModal && <RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />}
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<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" id="sidebar-collection-name">{collection.name}</div>
|
||||
@@ -104,17 +122,16 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
New Folder
|
||||
</div>
|
||||
{!isLocal ? (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRenameCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
) : null}
|
||||
{/* Todo: implement rename collection */}
|
||||
{/* <div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRenameCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</div> */}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
@@ -124,38 +141,15 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
Export
|
||||
</div>
|
||||
{!isLocal ? (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRemoveCollectionFromWSModal(true);
|
||||
}}
|
||||
>
|
||||
Remove from Workspace
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRemoveLocalCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
)}
|
||||
{!isLocal ? (
|
||||
<div
|
||||
className="dropdown-item delete-collection"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowDeleteCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRemoveCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,14 +157,13 @@ const Collection = ({ collection, searchText }) => {
|
||||
<div>
|
||||
{!collectionIsCollapsed ? (
|
||||
<div>
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '../../../../providers/Theme';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
import styled from 'styled-components';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LinkStyle = styled.span`
|
||||
color: ${(props) => props.theme['text-link']};
|
||||
`;
|
||||
|
||||
const CreateOrAddCollection = () => {
|
||||
const { theme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false);
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
|
||||
const handleCreateCollection = (values) => {
|
||||
setCreateCollectionModalOpen(false);
|
||||
dispatch(createCollection(values.collectionName))
|
||||
.then(() => {
|
||||
toast.success('Collection created');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while creating the collection'));
|
||||
};
|
||||
|
||||
const handleAddCollectionToWorkspace = (collectionUid) => {
|
||||
setAddCollectionToWSModalOpen(false);
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Collection added to workspace');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while adding collection to workspace'));
|
||||
};
|
||||
|
||||
const CreateLink = () => (
|
||||
<LinkStyle className="underline text-link cursor-pointer" theme={theme} onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
Create
|
||||
</LinkStyle>
|
||||
);
|
||||
const AddLink = () => (
|
||||
<LinkStyle className="underline text-link cursor-pointer" theme={theme} onClick={() => setAddCollectionToWSModalOpen(true)}>
|
||||
Add
|
||||
</LinkStyle>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 mt-4">
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} handleConfirm={handleCreateCollection} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
) : null}
|
||||
|
||||
<div className="text-xs text-center">
|
||||
<div>No collections found.</div>
|
||||
<div className="mt-2">
|
||||
<CreateLink /> or <AddLink /> Collection to Workspace.
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrAddCollection;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '../../../../providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { openCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
import styled from 'styled-components';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LinkStyle = styled.span`
|
||||
color: ${(props) => props.theme['text-link']};
|
||||
`;
|
||||
|
||||
const CreateOrOpenCollection = () => {
|
||||
const { theme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the collection'));
|
||||
};
|
||||
const CreateLink = () => (
|
||||
<LinkStyle className="underline text-link cursor-pointer" theme={theme} onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
Create
|
||||
</LinkStyle>
|
||||
);
|
||||
const OpenLink = () => (
|
||||
<LinkStyle className="underline text-link cursor-pointer" theme={theme} onClick={() => handleOpenCollection(true)}>
|
||||
Open
|
||||
</LinkStyle>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 mt-4">
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
|
||||
<div className="text-xs text-center">
|
||||
<div>No collections found.</div>
|
||||
<div className="mt-2">
|
||||
<CreateLink /> or <OpenLink /> Collection.
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrOpenCollection;
|
||||
@@ -1,21 +1,18 @@
|
||||
import React from 'react';
|
||||
import filter from 'lodash/filter';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconFiles } from '@tabler/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { isLocalCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SelectCollection = ({ onClose, onSelect, title }) => {
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const collectionsToDisplay = filter(collections, (c) => !isLocalCollection(c));
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={title || 'Select Collection'} hideFooter={true} handleCancel={onClose}>
|
||||
<ul className="mb-2">
|
||||
{collectionsToDisplay && collectionsToDisplay.length ? (
|
||||
collectionsToDisplay.map((c) => (
|
||||
{collections && collections.length ? (
|
||||
collections.map((c) => (
|
||||
<div className="collection" key={c.uid} onClick={() => onSelect(c.uid)}>
|
||||
<IconFiles size={18} strokeWidth={1.5} /> <span className="ml-2">{c.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-workspace {
|
||||
.collections-badge {
|
||||
margin-inline: 0.5rem;
|
||||
background-color: ${(props) => props.theme.sidebar.workspace.bg};
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 5px;
|
||||
|
||||
.caret {
|
||||
@@ -12,10 +12,6 @@ const Wrapper = styled.div`
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
}
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,36 +1,80 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import Collection from './Collection';
|
||||
import CreateOrAddCollection from './CreateOrAddCollection';
|
||||
import { findCollectionInWorkspace } from 'utils/workspaces';
|
||||
import { isLocalCollection } from 'utils/collections';
|
||||
|
||||
const Collections = ({ searchText }) => {
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const activeWorkspace = find(workspaces, (w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
if (!activeWorkspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const collectionToDisplay = filter(collections, (c) => findCollectionInWorkspace(activeWorkspace, c.uid) && !isLocalCollection(c));
|
||||
|
||||
if (!collectionToDisplay || !collectionToDisplay.length) {
|
||||
return <CreateOrAddCollection />;
|
||||
}
|
||||
import { IconSearch, IconFolders } from '@tabler/icons';
|
||||
import Collection from '../Collections/Collection';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
const CollectionsBadge = () => {
|
||||
return (
|
||||
<div className="mt-4 flex flex-col">
|
||||
{collectionToDisplay && collectionToDisplay.length
|
||||
? collectionToDisplay.map((c) => {
|
||||
return <Collection searchText={searchText} collection={c} key={c.uid} />;
|
||||
})
|
||||
: null}
|
||||
<div className="items-center mt-2 relative">
|
||||
<div className="collections-badge flex items-center pl-2 pr-2 py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Collections = () => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
|
||||
if (!collections || !collections.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<CollectionsBadge />
|
||||
<CreateOrOpenCollection />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
|
||||
<CollectionsBadge />
|
||||
|
||||
<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"
|
||||
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col">
|
||||
{collections && collections.length
|
||||
? collections.map((c) => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend} key={c.uid}>
|
||||
<Collection searchText={searchText} collection={c} key={c.uid} />
|
||||
</DndProvider>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Collections;
|
||||
|
||||
|
||||
@@ -2,29 +2,30 @@ import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browserLocalDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { createCollection, createLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const CreateCollection = ({ onClose, isLocal }) => {
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const isPlatformElectron = isElectron();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
collectionName: '',
|
||||
collectionFolderName: '',
|
||||
collectionLocation: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
collectionName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('collection name is required'),
|
||||
collectionFolderName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('folder name is required'),
|
||||
collectionLocation: Yup.string().required('location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
const action = isLocal && isPlatformElectron ? createLocalCollection : createCollection;
|
||||
dispatch(action(values.collectionName, values.collectionLocation))
|
||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||
.then(() => {
|
||||
toast.success('Collection created');
|
||||
onClose();
|
||||
@@ -34,7 +35,7 @@ const CreateCollection = ({ onClose, isLocal }) => {
|
||||
});
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browserLocalDirectory())
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
})
|
||||
@@ -56,8 +57,9 @@ const CreateCollection = ({ onClose, isLocal }) => {
|
||||
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
<label htmlFor="collectionName" className="flex items-center">
|
||||
<span className='font-semibold'>Name</span>
|
||||
<Tooltip text="Name of the collection" tooltipId="collection-name"/>
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
@@ -74,37 +76,52 @@ const CreateCollection = ({ onClose, isLocal }) => {
|
||||
/>
|
||||
{formik.touched.collectionName && formik.errors.collectionName ? <div className="text-red-500">{formik.errors.collectionName}</div> : null}
|
||||
|
||||
{isLocal && isPlatformElectron ? (
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{isLocal && isPlatformElectron && formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<label htmlFor="collectionFolderName" className="flex items-center mt-3">
|
||||
<span className='font-semibold'>Folder Name</span>
|
||||
<Tooltip text="Name of the folder where your collection is stored" tooltipId="collection-folder-name"/>
|
||||
</label>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
name="collectionFolderName"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={formik.handleChange}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionFolderName || ''}
|
||||
/>
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? <div className="text-red-500">{formik.errors.collectionFolderName}</div> : null}
|
||||
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
|
||||
{isLocal && isPlatformElectron ? (
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { collectionImported } from 'providers/ReduxStore/slices/collections';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportCollection = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
toast.success('Collection imported successfully');
|
||||
onClose();
|
||||
handleSubmit(collection);
|
||||
})
|
||||
.catch((err) => toastError(err, 'Import collection failed'));
|
||||
};
|
||||
@@ -26,10 +16,7 @@ const ImportCollection = ({ onClose }) => {
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
toast.success('Postman Collection imported successfully');
|
||||
onClose();
|
||||
handleSubmit(collection);
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
collectionLocation: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionLocation: Yup.string().min(1, 'must be atleast 1 characters').max(500, 'must be 500 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
console.log('here');
|
||||
handleSubmit(values.collectionLocation);
|
||||
}
|
||||
});
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
})
|
||||
.catch((error) => {
|
||||
formik.setFieldValue('collectionLocation', '');
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<div className='mt-2'>{collectionName}</div>
|
||||
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportCollectionLocation;
|
||||
@@ -1,26 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-workspace {
|
||||
margin-inline: 0.5rem;
|
||||
background-color: ${(props) => props.theme.sidebar.workspace.bg};
|
||||
border-radius: 5px;
|
||||
|
||||
.caret {
|
||||
margin-left: 0.25rem;
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
}
|
||||
|
||||
.muted-message {
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
|
||||
}
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { useState, useRef, forwardRef } from 'react';
|
||||
import filter from 'lodash/filter';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { openLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconArrowForwardUp, IconCaretDown, IconFolders, IconPlus } from '@tabler/icons';
|
||||
import Collection from '../Collections/Collection';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import { isLocalCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LocalCollections = ({ searchText }) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
|
||||
const collectionToDisplay = filter(collections, (c) => isLocalCollection(c));
|
||||
|
||||
if (!collectionToDisplay || !collectionToDisplay.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="current-workspace flex justify-between items-center pl-2 pr-2 py-1 select-none">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Local Collections</span>
|
||||
</div>
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const handleOpenLocalCollection = () => {
|
||||
dispatch(openLocalCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the local collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{createCollectionModalOpen ? <CreateCollection isLocal={true} onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
|
||||
<div className="items-center cursor-pointer mt-6 relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div className="dropdown-item" onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconPlus size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Create Collection</span>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={handleOpenLocalCollection}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconArrowForwardUp size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Open Collection</span>
|
||||
</div>
|
||||
|
||||
<div className="px-2 pt-2 muted-message" style={{ fontSize: 10 }}>
|
||||
Note: Local collections are not tied to a workspace
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col">
|
||||
{collectionToDisplay && collectionToDisplay.length
|
||||
? collectionToDisplay.map((c) => {
|
||||
return <Collection searchText={searchText} collection={c} key={c.uid} />;
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocalCollections;
|
||||
@@ -1,12 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.local-collections-unavailable {
|
||||
padding: 0.35rem 0.6rem;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
|
||||
font-size: 11px;
|
||||
}
|
||||
.collection-dropdown {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
|
||||
@@ -2,27 +2,36 @@ import toast from 'react-hot-toast';
|
||||
import Bruno from 'components/Bruno';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
|
||||
import { IconDots } from '@tabler/icons';
|
||||
import { IconFolders } from '@tabler/icons';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { useState, forwardRef, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { openLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TitleBar = () => {
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false);
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const isPlatformElectron = isElectron();
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportCollection = (collection) => {
|
||||
setImportedCollection(collection);
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation));
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
toast.success('Collection imported successfully');
|
||||
};
|
||||
|
||||
const menuDropdownTippyRef = useRef();
|
||||
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
@@ -35,27 +44,21 @@ const TitleBar = () => {
|
||||
|
||||
const handleTitleClick = () => dispatch(showHomePage());
|
||||
|
||||
const handleOpenLocalCollection = () => {
|
||||
dispatch(openLocalCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the local collection'));
|
||||
};
|
||||
|
||||
const handleAddCollectionToWorkspace = (collectionUid) => {
|
||||
setAddCollectionToWSModalOpen(false);
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Collection added to workspace');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while adding collection to workspace'));
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 py-2">
|
||||
{createCollectionModalOpen ? <CreateCollection isLocal={createCollectionModalOpen === 'local' ? true : false} onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
) : null}
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} /> : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
): null}
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center cursor-pointer" onClick={handleTitleClick}>
|
||||
@@ -73,12 +76,21 @@ const TitleBar = () => {
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setCreateCollectionModalOpen(true);
|
||||
menuDropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
Create Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
handleOpenCollection();
|
||||
menuDropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
Open Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
@@ -88,49 +100,6 @@ const TitleBar = () => {
|
||||
>
|
||||
Import Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setAddCollectionToWSModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Add Collection to Workspace
|
||||
</div>
|
||||
{isPlatformElectron ? (
|
||||
<>
|
||||
<div className="font-medium label-item font-medium local-collection-label">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Local Collections</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
setCreateCollectionModalOpen('local');
|
||||
menuDropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
Create Local Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
handleOpenLocalCollection();
|
||||
menuDropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
Open Local Collection
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center select-none text-xs local-collections-unavailable">
|
||||
Note: Local collections are only available on the desktop app.
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import MenuBar from './MenuBar';
|
||||
import TitleBar from './TitleBar';
|
||||
import Collections from './Collections';
|
||||
import LocalCollections from './LocalCollections';
|
||||
import StyledWrapper, { BottomWrapper, VersionNumber } from './StyledWrapper';
|
||||
import WorkspaceSelector from 'components/Workspaces/WorkspaceSelector';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconSearch, IconChevronsRight } from '@tabler/icons';
|
||||
import { IconChevronsRight } from '@tabler/icons';
|
||||
import { updateLeftSidebarWidth, updateIsDragging, toggleLeftMenuBar } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 222;
|
||||
@@ -21,7 +19,6 @@ const Sidebar = () => {
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragging) {
|
||||
@@ -82,30 +79,7 @@ const Sidebar = () => {
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col flex-grow">
|
||||
<TitleBar />
|
||||
<WorkspaceSelector />
|
||||
|
||||
<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"
|
||||
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Collections searchText={searchText} />
|
||||
<LocalCollections searchText={searchText} />
|
||||
<Collections />
|
||||
</div>
|
||||
|
||||
<div className="footer flex px-1 py-2 items-center cursor-pointer select-none">
|
||||
@@ -124,7 +98,7 @@ const Sidebar = () => {
|
||||
title="GitHub"
|
||||
></iframe>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.3.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.6.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
overflow-y: hidden;
|
||||
|
||||
.CodeMirror {
|
||||
background: transparent;
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
height: 20px !important;
|
||||
margin-top: 5px !important;
|
||||
border-left: 1px solid ${(props) => props.theme.text} !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
105
packages/bruno-app/src/components/SingleLineEditor/index.js
Normal file
105
packages/bruno-app/src/components/SingleLineEditor/index.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
class SingleLineEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.editorRef = React.createRef();
|
||||
this.variables = {};
|
||||
}
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
mode: "brunovariables",
|
||||
brunoVarInfo: {
|
||||
variables: getEnvironmentVariables(this.props.collection),
|
||||
},
|
||||
extraKeys: {
|
||||
"Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
"Ctrl-Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
"Cmd-Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
"Alt-Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
"Shift-Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Cmd-S': () => {
|
||||
console.log('cmd-s');
|
||||
if (this.props.onSave) {
|
||||
console.log('cmd-s +');
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': () => {},
|
||||
'Ctrl-F': () => {},
|
||||
'Tab': () => {}
|
||||
},
|
||||
});
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.on('change', (cm) => {
|
||||
this.props.onChange(cm.getValue());
|
||||
});
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.editor.getWrapperElement().remove();
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, "text/plain");
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper ref={this.editorRef} className="single-line-editor"></StyledWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default SingleLineEditor;
|
||||
16
packages/bruno-app/src/components/Tooltip/index.js
Normal file
16
packages/bruno-app/src/components/Tooltip/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Tooltip as ReactTooltip } from 'react-tooltip';
|
||||
|
||||
const Tooltip = ({ text, tooltipId }) => {
|
||||
return (
|
||||
<>
|
||||
<svg tabindex="-1" id={tooltipId} xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" className="inline-block ml-2 cursor-pointer" viewBox="0 0 16 16" style={{marginTop: 1}}>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
|
||||
</svg>
|
||||
<ReactTooltip anchorId={tooltipId} content={text} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
@@ -1,67 +1,56 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { collectionImported } from 'providers/ReduxStore/slices/collections';
|
||||
import { openLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFiles, IconFolders, IconPlayerPlay, IconBrandChrome, IconSpeakerphone, IconDeviceDesktop } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFolders, IconSpeakerphone } from '@tabler/icons';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import { importSampleCollection } from 'utils/importers/bruno-collection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Welcome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const isPlatformElectron = isElectron();
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
|
||||
const handleAddCollectionToWorkspace = (collectionUid) => {
|
||||
setAddCollectionToWSModalOpen(false);
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Collection added to workspace');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while adding collection to workspace'));
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the collection'));
|
||||
};
|
||||
|
||||
const handleImportSampleCollection = () => {
|
||||
importSampleCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
})
|
||||
.then(() => toast.success('Sample Collection loaded successfully'))
|
||||
.catch((err) => {
|
||||
toast.error('Load sample collection failed');
|
||||
console.log(err);
|
||||
});
|
||||
const handleImportCollection = (collection) => {
|
||||
setImportedCollection(collection);
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenLocalCollection = () => {
|
||||
dispatch(openLocalCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the local collection'));
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation));
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
toast.success('Collection imported successfully');
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 px-6 mt-6">
|
||||
{createCollectionModalOpen ? <CreateCollection isLocal={createCollectionModalOpen === 'local' ? true : false} onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
) : null}
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} /> : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
): null}
|
||||
|
||||
<div className="">
|
||||
<Bruno width={50} />
|
||||
</div>
|
||||
<div className="text-xl font-semibold select-none">bruno</div>
|
||||
<div className="mt-4">Local-first, Opensource API Client.</div>
|
||||
<div className="mt-4">Opensource IDE for exploring and testing api's</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
@@ -71,58 +60,22 @@ const Welcome = () => {
|
||||
Create Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={() => setAddCollectionToWSModalOpen(true)}>
|
||||
<IconFiles size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="add-collection">
|
||||
Add Collection to Workspace
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6">
|
||||
<IconFolders size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" onClick={handleOpenCollection}>
|
||||
Open Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
||||
<IconUpload size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="import-collection">Import Collection</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={handleImportSampleCollection}>
|
||||
<IconPlayerPlay size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="load-sample-collection">Load Sample Collection</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10 pt-6">Local Collections</div>
|
||||
{isPlatformElectron ? (
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
<div className="flex items-center">
|
||||
<IconPlus size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" onClick={() => setCreateCollectionModalOpen('local')}>
|
||||
Create Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6">
|
||||
<IconFolders size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" onClick={handleOpenLocalCollection}>
|
||||
Open Collection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="muted mt-4 flex items-center collection-options select-none text-gray-600 text-xs">Local collections are only available on the desktop app.</div>
|
||||
)}
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
|
||||
<div className="mt-4 flex flex-col collection-options select-none">
|
||||
<div>
|
||||
<a href="https://www.usebruno.com/downloads" target="_blank" className="flex items-center">
|
||||
<IconBrandChrome size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Chrome Extension</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://www.usebruno.com/downloads" target="_blank" className="flex items-center">
|
||||
<IconDeviceDesktop size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Desktop App</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-center">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
</a>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { useFormik } from 'formik';
|
||||
import { addWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const AddWorkspace = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(30, 'must be 30 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(addWorkspace(values.name))
|
||||
.then(() => {
|
||||
toast.success('Workspace created!');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while creating the workspace'));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Add Workspace'} confirmText="Add" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Workspace Name
|
||||
</label>
|
||||
<input
|
||||
id="workspace-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 || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWorkspace;
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
button.submit {
|
||||
color: white;
|
||||
background-color: var(--color-background-danger) !important;
|
||||
border: inherit !important;
|
||||
|
||||
&:hover {
|
||||
border: inherit !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { deleteWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteWorkspace = ({ onClose, workspace }) => {
|
||||
const dispatch = useDispatch();
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteWorkspace(workspace.uid))
|
||||
.then(() => {
|
||||
toast.success('Workspace deleted!');
|
||||
onClose();
|
||||
})
|
||||
.catch(toastError);
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Delete Workspace'} confirmText="Delete" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
Are you sure you want to delete <span className="font-semibold">{workspace.name}</span> ?
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteWorkspace;
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { useFormik } from 'formik';
|
||||
import { renameWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const EditWorkspace = ({ onClose, workspace }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: workspace.name
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(30, 'must be 30 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(renameWorkspace(values.name, workspace.uid))
|
||||
.then(() => {
|
||||
toast.success('Workspace renamed!');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while renaming the workspace'));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Rename Workspace'} confirmText="Rename" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Workspace Name
|
||||
</label>
|
||||
<input
|
||||
id="workspace-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 || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditWorkspace;
|
||||
@@ -1,17 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
div {
|
||||
padding: 4px 6px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
div:hover {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,26 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import EditWorkspace from '../EditWorkspace';
|
||||
import DeleteWorkspace from '../DeleteWorkspace';
|
||||
import { IconEdit, IconTrash } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WorkspaceItem = ({ workspace }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex justify-between items-baseline mb-2" key={workspace.uid}>
|
||||
<li>{workspace.name}</li>
|
||||
<div className="flex gap-x-4">
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
|
||||
</div>
|
||||
{openEditModal && <EditWorkspace onClose={() => setOpenEditModal(false)} workspace={workspace} />}
|
||||
{openDeleteModal && <DeleteWorkspace onClose={() => setOpenDeleteModal(false)} workspace={workspace} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceItem;
|
||||
@@ -1,19 +0,0 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import WorkspaceItem from './WorkspaceItem/index';
|
||||
import AddWorkspace from './AddWorkspace';
|
||||
|
||||
const WorkspaceConfigurer = ({ onClose }) => {
|
||||
const { workspaces } = useSelector((state) => state.workspaces);
|
||||
const [openAddModal, setOpenAddModal] = useState(false);
|
||||
|
||||
return (
|
||||
<Modal size="md" title="Workspaces" confirmText={'+ New Workspace'} handleConfirm={() => setOpenAddModal(true)} handleCancel={onClose} hideCancel={true}>
|
||||
<ul className="mb-2">{workspaces && workspaces.length && workspaces.map((workspace) => <WorkspaceItem workspace={workspace} key={workspace.uid} />)}</ul>
|
||||
{openAddModal && <AddWorkspace onClose={() => setOpenAddModal(false)} />}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceConfigurer;
|
||||
@@ -1,18 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.workspace {
|
||||
padding: 4px 6px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconBox } from '@tabler/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WorkspaceSelectModal = ({ onClose, onSelect, title }) => {
|
||||
const { workspaces } = useSelector((state) => state.workspaces);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={title || 'Select Workspace'} hideFooter={true} handleCancel={onClose}>
|
||||
<ul className="mb-2">
|
||||
{workspaces && workspaces.length ? (
|
||||
workspaces.map((w) => (
|
||||
<div className="workspace" key={w.uid} onClick={() => onSelect(w.uid)}>
|
||||
<IconBox size={18} strokeWidth={1.5} /> <span className="ml-2">{w.name}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>No workspaces found</div>
|
||||
)}
|
||||
</ul>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceSelectModal;
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useRef, forwardRef, useState, useEffect } from 'react';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconCaretDown, IconBox, IconSwitch3, IconSettings } from '@tabler/icons';
|
||||
import WorkspaceConfigurer from '../WorkspaceConfigurer';
|
||||
import WorkspaceSelectModal from '../WorkspaceSelectModal';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { selectWorkspace } from 'providers/ReduxStore/slices/workspaces';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WorkspaceSelector = () => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const [openWorkspacesModal, setOpenWorkspacesModal] = useState(false);
|
||||
const [openSwitchWorkspaceModal, setOpenSwitchWorkspaceModal] = useState(false);
|
||||
const [activeWorkspace, setActiveWorkspace] = useState({});
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveWorkspace(workspaces.find((workspace) => workspace.uid === activeWorkspaceUid));
|
||||
}, [activeWorkspaceUid, workspaces]);
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="current-workspace flex justify-between items-center pl-2 pr-2 py-1 select-none">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">
|
||||
<IconBox size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>{activeWorkspace ? activeWorkspace.name : ''}</span>
|
||||
</div>
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const handleSelectWorkspace = (workspaceUid) => {
|
||||
dispatch(selectWorkspace({ workspaceUid: workspaceUid }));
|
||||
setOpenSwitchWorkspaceModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{openWorkspacesModal && <WorkspaceConfigurer onClose={() => setOpenWorkspacesModal(false)} />}
|
||||
{openSwitchWorkspaceModal && <WorkspaceSelectModal onSelect={handleSelectWorkspace} title="Switch Workspace" onClose={() => setOpenSwitchWorkspaceModal(false)} />}
|
||||
|
||||
<div className="items-center cursor-pointer relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div className="dropdown-item" onClick={() => setOpenSwitchWorkspaceModal(true)}>
|
||||
<div className="pr-2 icon">
|
||||
<IconSwitch3 size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Switch Workspace</span>
|
||||
</div>
|
||||
|
||||
<div className="dropdown-item" onClick={() => setOpenWorkspacesModal(true)}>
|
||||
<div className="pr-2 icon">
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Configure Workspaces</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceSelector;
|
||||
@@ -124,6 +124,45 @@ const GlobalStyle = createGlobalStyle`
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
}
|
||||
|
||||
// codemirror
|
||||
.CodeMirror {
|
||||
.cm-variable-valid {
|
||||
color: ${(props) => props.theme.codemirror.variable.valid};
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid};
|
||||
}
|
||||
}
|
||||
.CodeMirror-brunoVarInfo {
|
||||
color: ${(props) => props.theme.codemirror.variable.info.color};
|
||||
background: ${(props) => props.theme.codemirror.variable.info.bg};
|
||||
border-radius: 2px;
|
||||
box-shadow: ${(props) => props.theme.codemirror.variable.info.boxShadow};
|
||||
box-sizing: border-box;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
margin: 8px -8px;
|
||||
max-width: 800px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
padding: 8px 8px;
|
||||
position: fixed;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.CodeMirror-brunoVarInfo :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-brunoVarInfo :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-brunoVarInfo p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlobalStyle;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconEdit, IconTrash } from '@tabler/icons';
|
||||
import RenameCollection from 'components/Sidebar/Collections/Collection/RenameCollection';
|
||||
import DeleteCollection from 'components/Sidebar/Collections/Collection/DeleteCollection';
|
||||
|
||||
export default function CollectionItem({ collection }) {
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
const [showDeleteCollectionModal, setShowDeleteCollectionModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showRenameCollectionModal && <RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />}
|
||||
{showDeleteCollectionModal && <DeleteCollection collection={collection} onClose={() => setShowDeleteCollectionModal(false)} />}
|
||||
<div className="flex justify-between items-baseline mb-2 collection-list-item">
|
||||
<li style={{ listStyle: 'none' }} className="collection-name">
|
||||
{collection.name}
|
||||
</li>
|
||||
<div className="flex gap-x-4">
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setShowRenameCollectionModal(true)} />
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setShowDeleteCollectionModal(true)} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.heading {
|
||||
display: inline-flex;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
padding: 6px 0px;
|
||||
border-bottom: 2px solid !important;
|
||||
}
|
||||
|
||||
.collection-list {
|
||||
min-width: 500px;
|
||||
|
||||
.collection-list-item {
|
||||
padding: 4px 0px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import filter from 'lodash/filter';
|
||||
import { useSelector } from 'react-redux';
|
||||
import CollectionItem from './CollectionItem';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isLocalCollection } from 'utils/collections';
|
||||
|
||||
export default function Collections() {
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const collectionsToDisplay = filter(collections, (c) => !isLocalCollection(c));
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h4 className="heading">Collections</h4>
|
||||
|
||||
<div className="collection-list mt-6">
|
||||
{collectionsToDisplay && collectionsToDisplay.length ? (
|
||||
collectionsToDisplay.map((collection) => {
|
||||
return <CollectionItem key={collection.uid} collection={collection} />;
|
||||
})
|
||||
) : (
|
||||
<div>No collections found</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -21,17 +21,6 @@ const Wrapper = styled.div`
|
||||
.fw-600 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.react-tabs {
|
||||
.react-tabs__tab-list {
|
||||
padding-left: 1rem;
|
||||
border-bottom: 1px solid #cfcfcf;
|
||||
|
||||
.react-tabs__tab--selected {
|
||||
border-color: #cfcfcf;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -16,6 +16,7 @@ if (!SERVER_RENDERED) {
|
||||
require('codemirror/addon/edit/matchbrackets');
|
||||
require('codemirror/addon/fold/brace-fold');
|
||||
require('codemirror/addon/fold/foldgutter');
|
||||
require('codemirror/addon/mode/overlay');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
require('codemirror/keymap/sublime');
|
||||
require('codemirror/addon/comment/comment');
|
||||
@@ -30,6 +31,8 @@ if (!SERVER_RENDERED) {
|
||||
require('codemirror-graphql/info');
|
||||
require('codemirror-graphql/jump');
|
||||
require('codemirror-graphql/mode');
|
||||
|
||||
require('utils/codemirror/brunoVarInfo');
|
||||
}
|
||||
|
||||
export default function Main() {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
.form-container {
|
||||
max-width: 350px;
|
||||
border-radius: 4px;
|
||||
border: 1px #ddd solid;
|
||||
|
||||
button.continue-btn {
|
||||
font-size: 16px;
|
||||
padding-top: 8x;
|
||||
padding-bottom: 8px;
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
color: #212529;
|
||||
background: #e2e6ea;
|
||||
border: solid 1px #dae0e5;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
font-size: 15px;
|
||||
color: rgb(192 69 8);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,243 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAuth } from 'providers/Auth';
|
||||
import AuthApi from 'api/auth';
|
||||
import { useFormik } from 'formik';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Login = () => {
|
||||
const router = useRouter();
|
||||
const [authState, authDispatch] = useAuth();
|
||||
|
||||
const { currentUser } = authState;
|
||||
|
||||
const [loggingIn, setLoggingIn] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
email: Yup.string().required('Email is required'),
|
||||
password: Yup.string().required('Password is required')
|
||||
}),
|
||||
onSubmit: (values, { resetForm }) => {
|
||||
setLoggingIn(true);
|
||||
AuthApi.login({
|
||||
email: values.email,
|
||||
password: values.password
|
||||
})
|
||||
.then((response) => {
|
||||
authDispatch({
|
||||
type: 'LOGIN_SUCCESS',
|
||||
user: response.data
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setLoggingIn(false);
|
||||
setLoginError(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (authState.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentUser) {
|
||||
router.push('/');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col justify-center cursor-pointer items-center mt-10">
|
||||
<div style={{ fontSize: '3rem' }}>
|
||||
<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>
|
||||
</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">
|
||||
<div className="text-2xl mt-3 mb-5">Login</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="pb-2 pt-3 block font-semibold">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="text"
|
||||
className="appearance-none rounded relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-blue-600 focus:z-10 sm:text-sm"
|
||||
placeholder="Email"
|
||||
onChange={formik.handleChange}
|
||||
onFocus={() => setLoginError(false)}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.email}
|
||||
/>
|
||||
{formik.touched.email && formik.errors.email ? <div className="field-error error-msg">{formik.errors.email}</div> : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="pb-2 pt-4 block font-semibold">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="password"
|
||||
className="appearance-none rounded relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-blue-600 focus:z-10 sm:text-sm"
|
||||
placeholder="Password"
|
||||
onChange={formik.handleChange}
|
||||
onFocus={() => setLoginError(false)}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.password}
|
||||
/>
|
||||
{formik.touched.password && formik.errors.password ? <div className="field-error error-msg">{formik.errors.password}</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
{loggingIn ? (
|
||||
<button
|
||||
disabled={true}
|
||||
className="continue-btn relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<div className="dot-elastic" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="continue-btn mb-4 relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{loginError ? <div className="field-error error-msg text-red-500 ml-1 mt-1">Invalid Credentials</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="sign-in-container mt-2"></div>
|
||||
<div className="mt-2">
|
||||
No account? <Link href="/signup">Create one!</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,49 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
.form-container {
|
||||
max-width: 350px;
|
||||
border-radius: 4px;
|
||||
border: 1px #ddd solid;
|
||||
|
||||
button.continue-btn {
|
||||
font-size: 16px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
color: #212529;
|
||||
background: #e2e6ea;
|
||||
border: solid 1px #dae0e5;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
|
||||
.or {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -14px;
|
||||
background: white;
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
font-size: 15px;
|
||||
color: rgb(192 69 8);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,273 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import AuthApi from 'api/auth';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAuth } from 'providers/Auth';
|
||||
|
||||
const SignUp = () => {
|
||||
const router = useRouter();
|
||||
const [authState, authDispatch] = useAuth();
|
||||
const [errorSigningUp, setErrorSigningUp] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [signingUp, setSigningUp] = useState(false);
|
||||
|
||||
const { currentUser } = authState;
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: '',
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(3, 'Must be atleast 3 characters').max(50, 'Must be 50 characters or less').required('Required'),
|
||||
email: Yup.string().email('Invalid email address').required('Required'),
|
||||
password: Yup.string().min(8, 'Must be atleast 8 characters').max(50, 'Must be 50 characters or less').required('Required')
|
||||
}),
|
||||
onSubmit: (values, { resetForm }) => {
|
||||
setSigningUp(true);
|
||||
AuthApi.signup({
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
password: values.password
|
||||
})
|
||||
.then((response) => {
|
||||
authDispatch({
|
||||
type: 'LOGIN_SUCCESS',
|
||||
user: response.data
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setSigningUp(false);
|
||||
setErrorSigningUp(true);
|
||||
setErrorMsg(error.message || 'An error occured during signup');
|
||||
});
|
||||
setSigningUp(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (authState.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentUser) {
|
||||
router.push('/');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col justify-center cursor-pointer items-center mt-10">
|
||||
<div style={{ fontSize: '3rem' }}>
|
||||
<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>
|
||||
</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">
|
||||
<div className="text-2xl mt-3 mb-5">Create Account</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="pb-2 pt-3 block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
className="appearance-none rounded relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-blue-600 focus:z-10 sm:text-sm"
|
||||
placeholder="Name"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.name}
|
||||
onFocus={() => setErrorSigningUp(false)}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="field-error error-msg">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email-address" className="pb-2 pt-4 block font-semibold">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email-address"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
className="appearance-none rounded relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-blue-600 focus:z-10 sm:text-sm"
|
||||
placeholder="Email address"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.email}
|
||||
onFocus={() => setErrorSigningUp(false)}
|
||||
/>
|
||||
{formik.touched.email && formik.errors.email ? <div className="field-error error-msg">{formik.errors.email}</div> : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="pb-2 pt-4 block font-semibold">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="password"
|
||||
className="appearance-none rounded relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-blue-600 focus:z-10 sm:text-sm"
|
||||
placeholder="At least 8 characters"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.password}
|
||||
onFocus={() => setErrorSigningUp(false)}
|
||||
/>
|
||||
{formik.touched.password && formik.errors.password ? <div className="field-error error-msg">{formik.errors.password}</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="py-4 text-xs">By signing in you are agreeing to our Terms of Use and our Privacy Policy.</div>
|
||||
|
||||
<div>
|
||||
{signingUp ? (
|
||||
<div>
|
||||
<button
|
||||
disabled={true}
|
||||
className="continue-btn relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<div className="dot-elastic" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{errorSigningUp ? <div className="field-error error-msg mb-2 text-red-500 ml-1 mt-1">{errorMsg}</div> : null}
|
||||
<div className="text-center pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="continue-btn mb-4 relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="sign-in-container mt-2"></div>
|
||||
<div className="mt-2">
|
||||
Already have an account? <Link href="/login">Log in</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUp;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { AppProvider } from 'providers/App';
|
||||
import { ToastProvider } from 'providers/Toaster';
|
||||
@@ -9,9 +10,9 @@ import ThemeProvider from 'providers/Theme/index';
|
||||
import '../styles/app.scss';
|
||||
import '../styles/globals.css';
|
||||
import 'tailwindcss/dist/tailwind.min.css';
|
||||
import 'react-tabs/style/react-tabs.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'graphiql/graphiql.min.css';
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
|
||||
function SafeHydrate({ children }) {
|
||||
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
|
||||
@@ -28,6 +29,16 @@ function NoSsr({ children }) {
|
||||
}
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [domLoaded, setDomLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDomLoaded(true);
|
||||
}, []);
|
||||
|
||||
if(!domLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeHydrate>
|
||||
<NoSsr>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import Head from 'next/head';
|
||||
import Collections from 'pageComponents/Collections';
|
||||
import MenuBar from 'components/Sidebar/MenuBar';
|
||||
import GlobalStyle from '../globalStyles';
|
||||
|
||||
export default function CollectionsPage() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>bruno</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<GlobalStyle />
|
||||
|
||||
<main>
|
||||
<div className="flex flex-row h-full">
|
||||
<MenuBar />
|
||||
<div className="flex flex-grow h-full px-8">
|
||||
<Collections />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import Head from 'next/head';
|
||||
import Login from 'pageComponents/Login';
|
||||
import MenuBar from 'components/Sidebar/MenuBar';
|
||||
import GlobalStyle from '../globalStyles';
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>bruno</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<GlobalStyle />
|
||||
|
||||
<main>
|
||||
<div className="flex flex-row h-full">
|
||||
<MenuBar />
|
||||
<div className="flex flex-grow h-full">
|
||||
<Login />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import Head from 'next/head';
|
||||
import SignUp from 'pageComponents/SignUp';
|
||||
import MenuBar from 'components/Sidebar/MenuBar';
|
||||
import GlobalStyle from '../globalStyles';
|
||||
|
||||
export default function SignUpPage() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>bruno</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<GlobalStyle />
|
||||
|
||||
<main>
|
||||
<div className="flex flex-row h-full">
|
||||
<MenuBar />
|
||||
<div className="flex flex-grow h-full">
|
||||
<SignUp />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import useIdb from './useIdb';
|
||||
import useTelemetry from './useTelemetry';
|
||||
import useLocalCollectionTreeSync from './useLocalCollectionTreeSync';
|
||||
import useCollectionTreeSync from './useCollectionTreeSync';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -9,9 +8,8 @@ import StyledWrapper from './StyledWrapper';
|
||||
export const AppContext = React.createContext();
|
||||
|
||||
export const AppProvider = (props) => {
|
||||
useIdb();
|
||||
useTelemetry();
|
||||
useLocalCollectionTreeSync();
|
||||
useCollectionTreeSync();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
localCollectionAddDirectoryEvent,
|
||||
localCollectionAddFileEvent,
|
||||
localCollectionChangeFileEvent,
|
||||
localCollectionUnlinkFileEvent,
|
||||
localCollectionUnlinkDirectoryEvent
|
||||
collectionAddDirectoryEvent,
|
||||
collectionAddFileEvent,
|
||||
collectionChangeFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionUnlinkEnvFileEvent
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import toast from 'react-hot-toast';
|
||||
import { openLocalCollectionEvent, localCollectionLoadEnvironmentsEvent } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
|
||||
const useLocalCollectionTreeSync = () => {
|
||||
const useCollectionTreeSync = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -23,27 +24,27 @@ const useLocalCollectionTreeSync = () => {
|
||||
|
||||
const _openCollection = (pathname, uid, name) => {
|
||||
console.log(`collection uid: ${uid}, pathname: ${pathname}, name: ${name}`);
|
||||
dispatch(openLocalCollectionEvent(uid, pathname, name));
|
||||
dispatch(openCollectionEvent(uid, pathname, name));
|
||||
};
|
||||
|
||||
const _collectionTreeUpdated = (type, val) => {
|
||||
if (type === 'addDir') {
|
||||
dispatch(
|
||||
localCollectionAddDirectoryEvent({
|
||||
collectionAddDirectoryEvent({
|
||||
dir: val
|
||||
})
|
||||
);
|
||||
}
|
||||
if (type === 'addFile') {
|
||||
dispatch(
|
||||
localCollectionAddFileEvent({
|
||||
collectionAddFileEvent({
|
||||
file: val
|
||||
})
|
||||
);
|
||||
}
|
||||
if (type === 'change') {
|
||||
dispatch(
|
||||
localCollectionChangeFileEvent({
|
||||
collectionChangeFileEvent({
|
||||
file: val
|
||||
})
|
||||
);
|
||||
@@ -51,7 +52,7 @@ const useLocalCollectionTreeSync = () => {
|
||||
if (type === 'unlink') {
|
||||
setTimeout(() => {
|
||||
dispatch(
|
||||
localCollectionUnlinkFileEvent({
|
||||
collectionUnlinkFileEvent({
|
||||
file: val
|
||||
})
|
||||
);
|
||||
@@ -59,21 +60,21 @@ const useLocalCollectionTreeSync = () => {
|
||||
}
|
||||
if (type === 'unlinkDir') {
|
||||
dispatch(
|
||||
localCollectionUnlinkDirectoryEvent({
|
||||
collectionUnlinkDirectoryEvent({
|
||||
directory: val
|
||||
})
|
||||
);
|
||||
}
|
||||
if (type === 'addEnvironmentFile') {
|
||||
dispatch(localCollectionLoadEnvironmentsEvent(val));
|
||||
dispatch(collectionAddEnvFileEvent(val));
|
||||
}
|
||||
if (type === 'changeEnvironmentFile') {
|
||||
dispatch(localCollectionLoadEnvironmentsEvent(val));
|
||||
if (type === 'unlinkEnvironmentFile') {
|
||||
dispatch(collectionUnlinkEnvFileEvent(val));
|
||||
}
|
||||
};
|
||||
|
||||
const _collectionAlreadyOpened = (pathname) => {
|
||||
toast.success('Collection is already opened under local collections');
|
||||
toast.success('Collection is already opened');
|
||||
};
|
||||
|
||||
const _displayError = (message) => {
|
||||
@@ -96,4 +97,4 @@ const useLocalCollectionTreeSync = () => {
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
||||
export default useLocalCollectionTreeSync;
|
||||
export default useCollectionTreeSync;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { openDB } from 'idb';
|
||||
import { idbConnectionReady } from 'providers/ReduxStore/slices/app';
|
||||
import { loadCollectionsFromIdb } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { loadWorkspacesFromIdb } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const useIdb = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
let dbName = `bruno`;
|
||||
let connection = openDB(dbName, 1, {
|
||||
upgrade(db, oldVersion, newVersion, transaction) {
|
||||
switch (oldVersion) {
|
||||
case 0:
|
||||
const collectionStore = db.createObjectStore('collection', { keyPath: 'uid' });
|
||||
const workspaceStore = db.createObjectStore('workspace', { keyPath: 'uid' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connection
|
||||
.then(() => {
|
||||
window.__idb = connection;
|
||||
dispatch(idbConnectionReady());
|
||||
dispatch(loadCollectionsFromIdb());
|
||||
dispatch(loadWorkspacesFromIdb());
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useIdb;
|
||||
@@ -2,14 +2,12 @@ import { configureStore } from '@reduxjs/toolkit';
|
||||
import appReducer from './slices/app';
|
||||
import collectionsReducer from './slices/collections';
|
||||
import tabsReducer from './slices/tabs';
|
||||
import workspacesReducer from './slices/workspaces';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
app: appReducer,
|
||||
collections: collectionsReducer,
|
||||
tabs: tabsReducer,
|
||||
workspaces: workspacesReducer
|
||||
tabs: tabsReducer
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,9 @@ import {
|
||||
addDepth,
|
||||
collapseCollection,
|
||||
deleteItemInCollection,
|
||||
isItemARequest
|
||||
deleteItemInCollectionByPathname,
|
||||
isItemARequest,
|
||||
areItemsTheSameExceptSeqUpdate
|
||||
} from 'utils/collections';
|
||||
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
||||
import { getSubdirectoriesFromRoot } from 'utils/common/platform';
|
||||
@@ -31,26 +33,6 @@ export const collectionsSlice = createSlice({
|
||||
name: 'collections',
|
||||
initialState,
|
||||
reducers: {
|
||||
loadCollections: (state, action) => {
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
each(action.payload.collections, (c) => collapseCollection(c));
|
||||
each(action.payload.collections, (c) => addDepth(c.items));
|
||||
each(action.payload.collections, (c) => {
|
||||
if (!collectionUids.includes(c.uid)) {
|
||||
state.collections.push(c);
|
||||
collectionUids.push(c.uid);
|
||||
}
|
||||
});
|
||||
},
|
||||
collectionImported: (state, action) => {
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const { collection } = action.payload;
|
||||
collapseCollection(collection);
|
||||
addDepth(collection.items);
|
||||
if (!collectionUids.includes(collection.uid)) {
|
||||
state.collections.push(collection);
|
||||
}
|
||||
},
|
||||
createCollection: (state, action) => {
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const collection = action.payload;
|
||||
@@ -67,7 +49,7 @@ export const collectionsSlice = createSlice({
|
||||
collection.name = action.payload.newName;
|
||||
}
|
||||
},
|
||||
deleteCollection: (state, action) => {
|
||||
removeCollection: (state, action) => {
|
||||
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
||||
},
|
||||
addEnvironment: (state, action) => {
|
||||
@@ -79,28 +61,12 @@ export const collectionsSlice = createSlice({
|
||||
collection.environments.push(environment);
|
||||
}
|
||||
},
|
||||
renameEnvironment: (state, action) => {
|
||||
const { newName, environmentUid, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
collectionUnlinkEnvFileEvent: (state, action) => {
|
||||
const { data: environment, meta } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, meta.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const environment = findEnvironmentInCollection(collection, environmentUid);
|
||||
|
||||
if (environment) {
|
||||
environment.name = newName;
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteEnvironment: (state, action) => {
|
||||
const { environmentUid, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const environment = findEnvironmentInCollection(collection, environmentUid);
|
||||
|
||||
if (environment) {
|
||||
collection.environments = filter(collection.environments, (e) => e.uid !== environmentUid);
|
||||
}
|
||||
collection.environments = filter(collection.environments, (e) => e.uid !== environment.uid);
|
||||
}
|
||||
},
|
||||
saveEnvironment: (state, action) => {
|
||||
@@ -182,12 +148,13 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
requestSent: (state, action) => {
|
||||
const { itemUid, collectionUid, cancelTokenUid } = action.payload;
|
||||
const { itemUid, collectionUid, cancelTokenUid, requestSent } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
item.requestSent = requestSent
|
||||
item.response = item.response || {};
|
||||
item.response.state = 'sending';
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
@@ -636,7 +603,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
localCollectionAddFileEvent: (state, action) => {
|
||||
collectionAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
|
||||
@@ -652,7 +619,7 @@ export const collectionsSlice = createSlice({
|
||||
uid: uuid(),
|
||||
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
||||
name: directoryName,
|
||||
collapsed: false,
|
||||
collapsed: true,
|
||||
type: 'folder',
|
||||
items: []
|
||||
};
|
||||
@@ -671,6 +638,7 @@ export const collectionsSlice = createSlice({
|
||||
if (currentItem) {
|
||||
currentItem.name = file.data.name;
|
||||
currentItem.type = file.data.type;
|
||||
currentItem.seq = file.data.seq;
|
||||
currentItem.request = file.data.request;
|
||||
currentItem.filename = file.meta.name;
|
||||
currentItem.pathname = file.meta.pathname;
|
||||
@@ -680,6 +648,7 @@ export const collectionsSlice = createSlice({
|
||||
uid: file.data.uid,
|
||||
name: file.data.name,
|
||||
type: file.data.type,
|
||||
seq: file.data.seq,
|
||||
request: file.data.request,
|
||||
filename: file.meta.name,
|
||||
pathname: file.meta.pathname,
|
||||
@@ -690,7 +659,7 @@ export const collectionsSlice = createSlice({
|
||||
addDepth(collection.items);
|
||||
}
|
||||
},
|
||||
localCollectionAddDirectoryEvent: (state, action) => {
|
||||
collectionAddDirectoryEvent: (state, action) => {
|
||||
const { dir } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, dir.meta.collectionUid);
|
||||
|
||||
@@ -705,7 +674,7 @@ export const collectionsSlice = createSlice({
|
||||
uid: uuid(),
|
||||
pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
|
||||
name: directoryName,
|
||||
collapsed: false,
|
||||
collapsed: true,
|
||||
type: 'folder',
|
||||
items: []
|
||||
};
|
||||
@@ -718,7 +687,7 @@ export const collectionsSlice = createSlice({
|
||||
addDepth(collection.items);
|
||||
}
|
||||
},
|
||||
localCollectionChangeFileEvent: (state, action) => {
|
||||
collectionChangeFileEvent: (state, action) => {
|
||||
const { file } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
|
||||
@@ -726,16 +695,24 @@ export const collectionsSlice = createSlice({
|
||||
const item = findItemInCollection(collection, file.data.uid);
|
||||
|
||||
if (item) {
|
||||
item.name = file.data.name;
|
||||
item.type = file.data.type;
|
||||
item.request = file.data.request;
|
||||
item.filename = file.meta.name;
|
||||
item.pathname = file.meta.pathname;
|
||||
item.draft = null;
|
||||
// whenever a user attempts to sort a req within the same folder
|
||||
// the seq is updated, but everything else remains the same
|
||||
// we don't want to lose the draft in this case
|
||||
if(areItemsTheSameExceptSeqUpdate(item, file.data)) {
|
||||
item.seq = file.data.seq;
|
||||
} else {
|
||||
item.name = file.data.name;
|
||||
item.type = file.data.type;
|
||||
item.seq = file.data.seq;
|
||||
item.request = file.data.request;
|
||||
item.filename = file.meta.name;
|
||||
item.pathname = file.meta.pathname;
|
||||
item.draft = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
localCollectionUnlinkFileEvent: (state, action) => {
|
||||
collectionUnlinkFileEvent: (state, action) => {
|
||||
const { file } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
|
||||
@@ -743,11 +720,11 @@ export const collectionsSlice = createSlice({
|
||||
const item = findItemInCollectionByPathname(collection, file.meta.pathname);
|
||||
|
||||
if (item) {
|
||||
deleteItemInCollection(item.uid, collection);
|
||||
deleteItemInCollectionByPathname(file.meta.pathname, collection);
|
||||
}
|
||||
}
|
||||
},
|
||||
localCollectionUnlinkDirectoryEvent: (state, action) => {
|
||||
collectionUnlinkDirectoryEvent: (state, action) => {
|
||||
const { directory } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, directory.meta.collectionUid);
|
||||
|
||||
@@ -755,30 +732,35 @@ export const collectionsSlice = createSlice({
|
||||
const item = findItemInCollectionByPathname(collection, directory.meta.pathname);
|
||||
|
||||
if (item) {
|
||||
deleteItemInCollection(item.uid, collection);
|
||||
deleteItemInCollectionByPathname(directory.meta.pathname, collection);
|
||||
}
|
||||
}
|
||||
},
|
||||
localCollectionLoadEnvironmentsEvent: (state, action) => {
|
||||
const { environments, collectionUid } = action.payload;
|
||||
collectionAddEnvFileEvent: (state, action) => {
|
||||
const { environment, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.environments = environments;
|
||||
collection.environments = collection.environments || [];
|
||||
|
||||
const existingEnv = collection.environments.find((e) => e.uid === environment.uid);
|
||||
|
||||
if (existingEnv) {
|
||||
existingEnv.variables = environment.variables;
|
||||
} else {
|
||||
collection.environments.push(environment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const {
|
||||
collectionImported,
|
||||
createCollection,
|
||||
renameCollection,
|
||||
deleteCollection,
|
||||
loadCollections,
|
||||
removeCollection,
|
||||
addEnvironment,
|
||||
renameEnvironment,
|
||||
deleteEnvironment,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
selectEnvironment,
|
||||
newItem,
|
||||
@@ -809,12 +791,12 @@ export const {
|
||||
updateRequestBody,
|
||||
updateRequestGraphqlQuery,
|
||||
updateRequestMethod,
|
||||
localCollectionAddFileEvent,
|
||||
localCollectionAddDirectoryEvent,
|
||||
localCollectionChangeFileEvent,
|
||||
localCollectionUnlinkFileEvent,
|
||||
localCollectionUnlinkDirectoryEvent,
|
||||
localCollectionLoadEnvironmentsEvent
|
||||
collectionAddFileEvent,
|
||||
collectionAddDirectoryEvent,
|
||||
collectionChangeFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import { uuid } from 'utils/common';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { workspaceSchema } from '@usebruno/schema';
|
||||
import { findCollectionInWorkspace } from 'utils/workspaces';
|
||||
import { getWorkspacesFromIdb, saveWorkspaceToIdb, deleteWorkspaceInIdb } from 'utils/idb/workspaces';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import {
|
||||
loadWorkspaces,
|
||||
addWorkspace as _addWorkspace,
|
||||
renameWorkspace as _renameWorkspace,
|
||||
deleteWorkspace as _deleteWorkspace,
|
||||
addCollectionToWorkspace as _addCollectionToWorkspace,
|
||||
removeCollectionFromWorkspace as _removeCollectionFromWorkspace
|
||||
} from './index';
|
||||
|
||||
const seedWorkpace = () => {
|
||||
const uid = uuid();
|
||||
const workspace = {
|
||||
uid: uid,
|
||||
name: 'My workspace',
|
||||
collections: []
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
workspaceSchema
|
||||
.validate(workspace)
|
||||
.then(() => saveWorkspaceToIdb(window.__idb, workspace))
|
||||
.then(() => resolve([workspace]))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadWorkspacesFromIdb = () => (dispatch) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
getWorkspacesFromIdb(window.__idb)
|
||||
.then((workspaces) => {
|
||||
if (!workspaces || !workspaces.length) {
|
||||
return seedWorkpace();
|
||||
}
|
||||
|
||||
return workspaces;
|
||||
})
|
||||
.then((workspaces) =>
|
||||
dispatch(
|
||||
loadWorkspaces({
|
||||
workspaces: workspaces
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const addWorkspace = (workspaceName) => (dispatch) => {
|
||||
const newWorkspace = {
|
||||
uid: uuid(),
|
||||
name: workspaceName,
|
||||
collections: []
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
workspaceSchema
|
||||
.validate(newWorkspace)
|
||||
.then(() => saveWorkspaceToIdb(window.__idb, newWorkspace))
|
||||
.then(() =>
|
||||
dispatch(
|
||||
_addWorkspace({
|
||||
workspace: newWorkspace
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const renameWorkspace = (newName, uid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const workspace = find(state.workspaces.workspaces, (w) => w.uid === uid);
|
||||
|
||||
if (!workspace) {
|
||||
return reject(new Error('Workspace not found'));
|
||||
}
|
||||
|
||||
const workspaceCopy = cloneDeep(workspace);
|
||||
workspaceCopy.name = newName;
|
||||
|
||||
workspaceSchema
|
||||
.validate(workspaceCopy)
|
||||
.then(() => saveWorkspaceToIdb(window.__idb, workspaceCopy))
|
||||
.then(() =>
|
||||
dispatch(
|
||||
_renameWorkspace({
|
||||
uid: uid,
|
||||
name: newName
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteWorkspace = (workspaceUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (state.workspaces.activeWorkspaceUid === workspaceUid) {
|
||||
throw new BrunoError('Cannot delete current workspace');
|
||||
}
|
||||
|
||||
const workspace = find(state.workspaces.workspaces, (w) => w.uid === workspaceUid);
|
||||
|
||||
if (!workspace) {
|
||||
return reject(new Error('Workspace not found'));
|
||||
}
|
||||
|
||||
deleteWorkspaceInIdb(window.__idb, workspaceUid)
|
||||
.then(() =>
|
||||
dispatch(
|
||||
_deleteWorkspace({
|
||||
workspaceUid: workspaceUid
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const addCollectionToWorkspace = (workspaceUid, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const workspace = find(state.workspaces.workspaces, (w) => w.uid === workspaceUid);
|
||||
const collection = find(state.collections.collections, (c) => c.uid === collectionUid);
|
||||
|
||||
if (!workspace) {
|
||||
return reject(new Error('Workspace not found'));
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const workspaceCopy = cloneDeep(workspace);
|
||||
if (workspaceCopy.collections && workspace.collections.length) {
|
||||
if (!findCollectionInWorkspace(workspace, collectionUid)) {
|
||||
workspaceCopy.collections.push({
|
||||
uid: collectionUid
|
||||
});
|
||||
}
|
||||
} else {
|
||||
workspaceCopy.collections = [
|
||||
{
|
||||
uid: collectionUid
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
workspaceSchema
|
||||
.validate(workspaceCopy)
|
||||
.then(() => saveWorkspaceToIdb(window.__idb, workspaceCopy))
|
||||
.then(() =>
|
||||
dispatch(
|
||||
_addCollectionToWorkspace({
|
||||
workspaceUid: workspaceUid,
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const removeCollectionFromWorkspace = (workspaceUid, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const workspace = find(state.workspaces.workspaces, (w) => w.uid === workspaceUid);
|
||||
const collection = find(state.collections.collections, (c) => c.uid === collectionUid);
|
||||
|
||||
if (!workspace) {
|
||||
return reject(new Error('Workspace not found'));
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const workspaceCopy = cloneDeep(workspace);
|
||||
if (workspaceCopy.collections && workspace.collections.length) {
|
||||
workspaceCopy.collections = filter(workspaceCopy.collections, (c) => c.uid !== collectionUid);
|
||||
}
|
||||
|
||||
workspaceSchema
|
||||
.validate(workspaceCopy)
|
||||
.then(() => saveWorkspaceToIdb(window.__idb, workspaceCopy))
|
||||
.then(() =>
|
||||
dispatch(
|
||||
_removeCollectionFromWorkspace({
|
||||
workspaceUid: workspaceUid,
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
// TODO
|
||||
// Workspaces can have collection uids that no longer exist
|
||||
// or the user may have the collections access revoked (in teams)
|
||||
// This action will have to be called at the beginning to purge any zombi collection references in the workspaces
|
||||
export const removeZombieCollectionFromAllWorkspaces = (workspaceUid) => (dispatch, getState) => {};
|
||||
@@ -1,85 +0,0 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import find from 'lodash/find';
|
||||
import map from 'lodash/map';
|
||||
import filter from 'lodash/filter';
|
||||
import { findCollectionInWorkspace } from 'utils/workspaces';
|
||||
import cache from 'utils/common/cache';
|
||||
|
||||
const initialState = {
|
||||
workspaces: [],
|
||||
activeWorkspaceUid: null
|
||||
};
|
||||
|
||||
export const workspacesSlice = createSlice({
|
||||
name: 'workspaces',
|
||||
initialState,
|
||||
reducers: {
|
||||
loadWorkspaces: (state, action) => {
|
||||
state.workspaces = action.payload.workspaces;
|
||||
|
||||
if (state.workspaces && state.workspaces.length) {
|
||||
const workspaceUids = map(state.workspaces, (w) => w.uid);
|
||||
const activeWorkspaceUid = cache.getActiveWorkspaceUid();
|
||||
if (activeWorkspaceUid && workspaceUids.includes(activeWorkspaceUid)) {
|
||||
state.activeWorkspaceUid = activeWorkspaceUid;
|
||||
} else {
|
||||
state.activeWorkspaceUid = state.workspaces[0].uid;
|
||||
cache.setActiveWorkspaceUid(state.activeWorkspaceUid);
|
||||
}
|
||||
}
|
||||
},
|
||||
selectWorkspace: (state, action) => {
|
||||
state.activeWorkspaceUid = action.payload.workspaceUid;
|
||||
cache.setActiveWorkspaceUid(state.activeWorkspaceUid);
|
||||
},
|
||||
renameWorkspace: (state, action) => {
|
||||
const { name, uid } = action.payload;
|
||||
const workspace = find(state.workspaces, (w) => w.uid === uid);
|
||||
|
||||
if (workspace) {
|
||||
workspace.name = name;
|
||||
}
|
||||
},
|
||||
deleteWorkspace: (state, action) => {
|
||||
if (state.activeWorkspaceUid === action.payload.workspaceUid) {
|
||||
throw new Error('User cannot delete current workspace');
|
||||
}
|
||||
state.workspaces = state.workspaces.filter((workspace) => workspace.uid !== action.payload.workspaceUid);
|
||||
},
|
||||
addWorkspace: (state, action) => {
|
||||
state.workspaces.push(action.payload.workspace);
|
||||
},
|
||||
addCollectionToWorkspace: (state, action) => {
|
||||
const { workspaceUid, collectionUid } = action.payload;
|
||||
const workspace = find(state.workspaces, (w) => w.uid === workspaceUid);
|
||||
|
||||
if (workspace) {
|
||||
if (workspace.collections && workspace.collections.length) {
|
||||
if (!findCollectionInWorkspace(workspace, collectionUid)) {
|
||||
workspace.collections.push({
|
||||
uid: collectionUid
|
||||
});
|
||||
}
|
||||
} else {
|
||||
workspace.collections = [
|
||||
{
|
||||
uid: collectionUid
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
},
|
||||
removeCollectionFromWorkspace: (state, action) => {
|
||||
const { workspaceUid, collectionUid } = action.payload;
|
||||
const workspace = find(state.workspaces, (w) => w.uid === workspaceUid);
|
||||
|
||||
if (workspace && workspace.collections && workspace.collections.length) {
|
||||
workspace.collections = filter(workspace.collections, (c) => c.uid !== collectionUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { loadWorkspaces, selectWorkspace, renameWorkspace, deleteWorkspace, addWorkspace, addCollectionToWorkspace, removeCollectionFromWorkspace } = workspacesSlice.actions;
|
||||
|
||||
export default workspacesSlice.reducer;
|
||||
@@ -6,8 +6,10 @@ const darkTheme = {
|
||||
|
||||
colors: {
|
||||
text: {
|
||||
green: 'rgb(11 178 126)',
|
||||
danger: '#f06f57',
|
||||
muted: '#9d9d9d'
|
||||
muted: '#9d9d9d',
|
||||
purple: '#cd56d6'
|
||||
},
|
||||
bg: {
|
||||
danger: '#d03544'
|
||||
@@ -24,7 +26,7 @@ const darkTheme = {
|
||||
bg: '#252526',
|
||||
dragbar: '#8a8a8a',
|
||||
|
||||
workspace: {
|
||||
badge: {
|
||||
bg: '#3D3D3D'
|
||||
},
|
||||
|
||||
@@ -186,6 +188,15 @@ const darkTheme = {
|
||||
border: 'transparent',
|
||||
gutter: {
|
||||
bg: '#1e1e1e'
|
||||
},
|
||||
variable: {
|
||||
valid: 'rgb(11 178 126)',
|
||||
invalid: '#f06f57',
|
||||
info: {
|
||||
color: '#ce9178',
|
||||
bg: 'rgb(48,48,49)',
|
||||
boxShadow: 'rgb(0 0 0 / 36%) 0px 2px 8px'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ const lightTheme = {
|
||||
|
||||
colors: {
|
||||
text: {
|
||||
green: '#047857',
|
||||
danger: 'rgb(185, 28, 28)',
|
||||
muted: '#4b5563'
|
||||
muted: '#4b5563',
|
||||
purple: '#8e44ad',
|
||||
},
|
||||
bg: {
|
||||
danger: '#dc3545'
|
||||
@@ -24,7 +26,7 @@ const lightTheme = {
|
||||
bg: '#F3F3F3',
|
||||
dragbar: 'rgb(200, 200, 200)',
|
||||
|
||||
workspace: {
|
||||
badge: {
|
||||
bg: '#e1e1e1'
|
||||
},
|
||||
|
||||
@@ -190,6 +192,15 @@ const lightTheme = {
|
||||
border: '#efefef',
|
||||
gutter: {
|
||||
bg: '#f3f3f3'
|
||||
},
|
||||
variable: {
|
||||
valid: '#047857',
|
||||
invalid: 'rgb(185, 28, 28)',
|
||||
info: {
|
||||
color: 'rgb(52, 52, 52)',
|
||||
bg: 'white',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.45)'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
199
packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
Normal file
199
packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Copyright (c) 2017, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3
|
||||
*/
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
|
||||
const renderVarInfo = (token, options, cm, pos) => {
|
||||
const str = token.string || '';
|
||||
if(!str || !str.length || typeof str !== 'string') {
|
||||
return;
|
||||
}
|
||||
// str is of format {{variableName}}, extract variableName
|
||||
// we are seeing that from the gql query editor, the token string is of format variableName
|
||||
const variableName = str.replace('{{', '').replace('}}', '').trim();
|
||||
const variableValue = options.variables[variableName];
|
||||
|
||||
const into = document.createElement('div');
|
||||
const descriptionDiv = document.createElement('div');
|
||||
descriptionDiv.className = 'info-description';
|
||||
|
||||
descriptionDiv.appendChild(document.createTextNode(variableValue));
|
||||
into.appendChild(descriptionDiv);
|
||||
|
||||
return into;
|
||||
};
|
||||
|
||||
CodeMirror.defineOption('brunoVarInfo', false, function(cm, options, old) {
|
||||
|
||||
if (old && old !== CodeMirror.Init) {
|
||||
const oldOnMouseOver = cm.state.brunoVarInfo.onMouseOver;
|
||||
CodeMirror.off(cm.getWrapperElement(), 'mouseover', oldOnMouseOver);
|
||||
clearTimeout(cm.state.brunoVarInfo.hoverTimeout);
|
||||
delete cm.state.brunoVarInfo;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
const state = (cm.state.brunoVarInfo = createState(options));
|
||||
state.onMouseOver = onMouseOver.bind(null, cm);
|
||||
CodeMirror.on(cm.getWrapperElement(), 'mouseover', state.onMouseOver);
|
||||
}
|
||||
});
|
||||
|
||||
function createState(options) {
|
||||
return {
|
||||
options:
|
||||
options instanceof Function
|
||||
? {render: options}
|
||||
: options === true ? {} : options,
|
||||
};
|
||||
}
|
||||
|
||||
function getHoverTime(cm) {
|
||||
const options = cm.state.brunoVarInfo.options;
|
||||
return (options && options.hoverTime) || 50;
|
||||
}
|
||||
|
||||
function onMouseOver(cm, e) {
|
||||
const state = cm.state.brunoVarInfo;
|
||||
const target = e.target || e.srcElement;
|
||||
|
||||
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(target.className !== 'cm-variable-valid') {
|
||||
return;
|
||||
}
|
||||
|
||||
const box = target.getBoundingClientRect();
|
||||
|
||||
const hoverTime = getHoverTime(cm);
|
||||
state.hoverTimeout = setTimeout(onHover, hoverTime);
|
||||
|
||||
const onMouseMove = function() {
|
||||
clearTimeout(state.hoverTimeout);
|
||||
state.hoverTimeout = setTimeout(onHover, hoverTime);
|
||||
};
|
||||
|
||||
const onMouseOut = function() {
|
||||
CodeMirror.off(document, 'mousemove', onMouseMove);
|
||||
CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut);
|
||||
clearTimeout(state.hoverTimeout);
|
||||
state.hoverTimeout = undefined;
|
||||
};
|
||||
|
||||
const onHover = function() {
|
||||
CodeMirror.off(document, 'mousemove', onMouseMove);
|
||||
CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut);
|
||||
state.hoverTimeout = undefined;
|
||||
onMouseHover(cm, box);
|
||||
};
|
||||
|
||||
CodeMirror.on(document, 'mousemove', onMouseMove);
|
||||
CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut);
|
||||
}
|
||||
|
||||
function onMouseHover(cm, box) {
|
||||
const pos = cm.coordsChar({
|
||||
left: (box.left + box.right) / 2,
|
||||
top: (box.top + box.bottom) / 2,
|
||||
});
|
||||
|
||||
const state = cm.state.brunoVarInfo;
|
||||
const options = state.options;
|
||||
const token = cm.getTokenAt(pos, true);
|
||||
if (token) {
|
||||
const brunoVarInfo = renderVarInfo(token, options, cm, pos);
|
||||
if (brunoVarInfo) {
|
||||
showPopup(cm, box, brunoVarInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showPopup(cm, box, brunoVarInfo) {
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'CodeMirror-brunoVarInfo';
|
||||
popup.appendChild(brunoVarInfo);
|
||||
document.body.appendChild(popup);
|
||||
|
||||
const popupBox = popup.getBoundingClientRect();
|
||||
const popupStyle = popup.currentStyle || window.getComputedStyle(popup);
|
||||
const popupWidth =
|
||||
popupBox.right -
|
||||
popupBox.left +
|
||||
parseFloat(popupStyle.marginLeft) +
|
||||
parseFloat(popupStyle.marginRight);
|
||||
const popupHeight =
|
||||
popupBox.bottom -
|
||||
popupBox.top +
|
||||
parseFloat(popupStyle.marginTop) +
|
||||
parseFloat(popupStyle.marginBottom);
|
||||
|
||||
let topPos = box.bottom;
|
||||
if (
|
||||
popupHeight > window.innerHeight - box.bottom - 15 &&
|
||||
box.top > window.innerHeight - box.bottom
|
||||
) {
|
||||
topPos = box.top - popupHeight;
|
||||
}
|
||||
|
||||
if (topPos < 0) {
|
||||
topPos = box.bottom;
|
||||
}
|
||||
|
||||
// make popup appear on top of cursor
|
||||
if (topPos > 70) {
|
||||
topPos = topPos - 70;
|
||||
}
|
||||
|
||||
let leftPos = Math.max(0, window.innerWidth - popupWidth - 15);
|
||||
if (leftPos > box.left) {
|
||||
leftPos = box.left;
|
||||
}
|
||||
|
||||
popup.style.opacity = 1;
|
||||
popup.style.top = topPos + 'px';
|
||||
popup.style.left = leftPos + 'px';
|
||||
|
||||
let popupTimeout;
|
||||
|
||||
const onMouseOverPopup = function() {
|
||||
clearTimeout(popupTimeout);
|
||||
};
|
||||
|
||||
const onMouseOut = function() {
|
||||
clearTimeout(popupTimeout);
|
||||
popupTimeout = setTimeout(hidePopup, 200);
|
||||
};
|
||||
|
||||
const hidePopup = function() {
|
||||
CodeMirror.off(popup, 'mouseover', onMouseOverPopup);
|
||||
CodeMirror.off(popup, 'mouseout', onMouseOut);
|
||||
CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut);
|
||||
|
||||
if (popup.style.opacity) {
|
||||
popup.style.opacity = 0;
|
||||
setTimeout(function() {
|
||||
if (popup.parentNode) {
|
||||
popup.parentNode.removeChild(popup);
|
||||
}
|
||||
}, 600);
|
||||
} else if (popup.parentNode) {
|
||||
popup.parentNode.removeChild(popup);
|
||||
}
|
||||
};
|
||||
|
||||
CodeMirror.on(popup, 'mouseover', onMouseOverPopup);
|
||||
CodeMirror.on(popup, 'mouseout', onMouseOut);
|
||||
CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,37 @@
|
||||
import * as FileSaver from 'file-saver';
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
|
||||
const deleteUidsInItems = (items) => {
|
||||
each(items, (item) => {
|
||||
delete item.uid;
|
||||
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
each(get(item, 'request.headers'), (header) => delete header.uid);
|
||||
each(get(item, 'request.params'), (param) => delete param.uid);
|
||||
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
deleteUidsInItems(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUidsInEnvs = (envs) => {
|
||||
each(envs, (env) => {
|
||||
delete env.uid;
|
||||
each(env.variables, (variable) => delete variable.uid);
|
||||
});
|
||||
};
|
||||
|
||||
const exportCollection = (collection) => {
|
||||
// delete uids
|
||||
delete collection.uid;
|
||||
deleteUidsInItems(collection.items);
|
||||
deleteUidsInEnvs(collection.environments);
|
||||
|
||||
const fileName = `${collection.name}.json`;
|
||||
const fileBlob = new Blob([JSON.stringify(collection, null, 2)], { type: 'application/json' });
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import reckon from 'reckonjs';
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import find from 'lodash/find';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import isString from 'lodash/isString';
|
||||
import map from 'lodash/map';
|
||||
import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'path';
|
||||
|
||||
// although we are not using rekonjs directly
|
||||
// its populating the global string prototype with .reckon method
|
||||
import reckon from 'reckonjs';
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if (!str || !str.length || !isString(str)) {
|
||||
@@ -122,6 +129,80 @@ export const findEnvironmentInCollection = (collection, envUid) => {
|
||||
return find(collection.environments, (e) => e.uid === envUid);
|
||||
};
|
||||
|
||||
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
if (draggedItemParent) {
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||
}
|
||||
|
||||
if (targetItem.type === 'folder') {
|
||||
targetItem.items = targetItem.items || [];
|
||||
targetItem.items.push(draggedItem);
|
||||
draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename);
|
||||
} else {
|
||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||
|
||||
if (targetItemParent) {
|
||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
// If the dragged item is already at the root of the collection, do nothing
|
||||
if(!draggedItemParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
collection.items.push(draggedItem);
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
};
|
||||
|
||||
export const getItemsToResequence = (parent, collection) => {
|
||||
let itemsToResequence = [];
|
||||
|
||||
if(!parent) {
|
||||
let index = 1;
|
||||
each(collection.items, (item) => {
|
||||
if(isItemARequest(item)) {
|
||||
itemsToResequence.push({
|
||||
pathname: item.pathname,
|
||||
seq: index++
|
||||
});
|
||||
}
|
||||
});
|
||||
return itemsToResequence;
|
||||
}
|
||||
|
||||
if (parent.items && parent.items.length) {
|
||||
let index = 1;
|
||||
each(parent.items, (item) => {
|
||||
if(isItemARequest(item)) {
|
||||
itemsToResequence.push({
|
||||
pathname: item.pathname,
|
||||
seq: index++
|
||||
});
|
||||
}
|
||||
});
|
||||
return itemsToResequence;
|
||||
}
|
||||
|
||||
return itemsToResequence;
|
||||
};
|
||||
|
||||
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
const copyHeaders = (headers) => {
|
||||
return map(headers, (header) => {
|
||||
@@ -175,11 +256,11 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
each(sourceItems, (si) => {
|
||||
const di = {
|
||||
uid: si.uid,
|
||||
type: si.type
|
||||
type: si.type,
|
||||
name: si.name,
|
||||
seq: si.seq
|
||||
};
|
||||
|
||||
di.name = si.name;
|
||||
|
||||
// if items is draft, then take data from draft to save
|
||||
// The condition "!options.ignoreDraft" may appear confusing
|
||||
// When saving a collection, this option allows the caller to specify to ignore any draft changes while still saving rest of the collection.
|
||||
@@ -256,6 +337,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
uid: _item.uid,
|
||||
type: _item.type,
|
||||
name: _item.name,
|
||||
seq: _item.seq,
|
||||
request: {
|
||||
method: _item.request.method,
|
||||
url: _item.request.url,
|
||||
@@ -297,7 +379,6 @@ export const deleteItemInCollection = (itemUid, collection) => {
|
||||
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
|
||||
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
each(flattenedItems, (i) => {
|
||||
if (i.items && i.items.length) {
|
||||
i.items = filter(i.items, (i) => i.uid !== itemUid);
|
||||
@@ -305,6 +386,17 @@ export const deleteItemInCollection = (itemUid, collection) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteItemInCollectionByPathname = (pathname, collection) => {
|
||||
collection.items = filter(collection.items, (i) => i.pathname !== pathname);
|
||||
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
each(flattenedItems, (i) => {
|
||||
if (i.items && i.items.length) {
|
||||
i.items = filter(i.items, (i) => i.pathname !== pathname);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const isItemARequest = (item) => {
|
||||
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type) && !item.items;
|
||||
};
|
||||
@@ -376,7 +468,6 @@ export const interpolateEnvironmentVars = (item, variables) => {
|
||||
};
|
||||
|
||||
request.url = interpolate(request.url);
|
||||
console.log(request.url);
|
||||
|
||||
each(request.headers, (header) => {
|
||||
header.value = interpolate(header.value);
|
||||
@@ -412,6 +503,44 @@ export const interpolateEnvironmentVars = (item, variables) => {
|
||||
return request;
|
||||
};
|
||||
|
||||
export const deleteUidsInItem = (item) => {
|
||||
delete item.uid;
|
||||
const params = get(item, 'request.params', []);
|
||||
const headers = get(item, 'request.headers', []);
|
||||
const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);
|
||||
const bodyMultipartForm = get(item, 'request.body.multipartForm', []);
|
||||
|
||||
params.forEach((param) => delete param.uid);
|
||||
headers.forEach((header) => delete header.uid);
|
||||
bodyFormUrlEncoded.forEach((param) => delete param.uid);
|
||||
bodyMultipartForm.forEach((param) => delete param.uid);
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
export const areItemsTheSameExceptSeqUpdate = (_item1, _item2) => {
|
||||
let item1 = cloneDeep(_item1);
|
||||
let item2 = cloneDeep(_item2);
|
||||
|
||||
// remove seq from both items
|
||||
delete item1.seq;
|
||||
delete item2.seq;
|
||||
|
||||
// remove draft from both items
|
||||
delete item1.draft;
|
||||
delete item2.draft;
|
||||
|
||||
// get projection of both items
|
||||
item1 = transformRequestToSaveToFilesystem(item1);
|
||||
item2 = transformRequestToSaveToFilesystem(item2);
|
||||
|
||||
// delete uids from both items
|
||||
deleteUidsInItem(item1);
|
||||
deleteUidsInItem(item2);
|
||||
|
||||
return isEqual(item1, item2);
|
||||
};
|
||||
|
||||
export const getDefaultRequestPaneTab = (item) => {
|
||||
if(item.type === 'http-request') {
|
||||
return 'params';
|
||||
@@ -421,3 +550,19 @@ export const getDefaultRequestPaneTab = (item) => {
|
||||
return 'query';
|
||||
}
|
||||
};
|
||||
|
||||
export const getEnvironmentVariables = (collection) => {
|
||||
let variables = {};
|
||||
if (collection) {
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if (environment) {
|
||||
each(environment.variables, (variable) => {
|
||||
if(variable.name && variable.value && variable.enabled) {
|
||||
variables[variable.name] = variable.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
@@ -5,14 +5,6 @@ class Cache {
|
||||
set(key, val) {
|
||||
window.localStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
getActiveWorkspaceUid() {
|
||||
return this.get('bruno.activeWorkspaceUid');
|
||||
}
|
||||
|
||||
setActiveWorkspaceUid(workspaceUid) {
|
||||
this.set('bruno.activeWorkspaceUid', workspaceUid);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Cache();
|
||||
|
||||
35
packages/bruno-app/src/utils/common/codemirror.js
Normal file
35
packages/bruno-app/src/utils/common/codemirror.js
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
CodeMirror.defineMode("brunovariables", function(config, parserConfig) {
|
||||
let variablesOverlay = {
|
||||
token: function(stream, state) {
|
||||
if (stream.match("{{", true)) {
|
||||
let ch;
|
||||
let word = "";
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch == "}" && stream.next() == "}") {
|
||||
stream.eat("}");
|
||||
if (word in variables) {
|
||||
return "variable-valid";
|
||||
} else {
|
||||
return "variable-invalid";
|
||||
}
|
||||
}
|
||||
word += ch;
|
||||
}
|
||||
}
|
||||
while (stream.next() != null && !stream.match("{{", false)) {}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
||||
});
|
||||
};
|
||||
@@ -24,3 +24,25 @@ export const waitForNextTick = () => {
|
||||
setTimeout(() => resolve(), 0);
|
||||
});
|
||||
};
|
||||
|
||||
export const safeParseJSON = (str) => {
|
||||
if(!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
export const safeStringifyJSON = (obj) => {
|
||||
if(!obj) {
|
||||
return obj;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(obj);
|
||||
} catch (e) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const isLocalCollection = (collection) => {
|
||||
};
|
||||
|
||||
export const resolveRequestFilename = (name) => {
|
||||
return `${trim(name)}.json`;
|
||||
return `${trim(name)}.bru`;
|
||||
};
|
||||
|
||||
export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
|
||||
|
||||
@@ -13,20 +13,6 @@ export const saveCollectionToIdb = (connection, collection) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCollectionInIdb = (connection, collectionUid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection
|
||||
.then((db) => {
|
||||
let tx = db.transaction(`collection`, 'readwrite');
|
||||
tx.objectStore('collection').delete(collectionUid);
|
||||
|
||||
tx.oncomplete = () => resolve(collectionUid);
|
||||
tx.onerror = () => reject(tx.error);
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const getCollectionsFromIdb = (connection) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import isArray from 'lodash/isArray';
|
||||
|
||||
export const saveWorkspaceToIdb = (connection, workspace) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection
|
||||
.then((db) => {
|
||||
let tx = db.transaction(`workspace`, 'readwrite');
|
||||
let workspaceStore = tx.objectStore('workspace');
|
||||
|
||||
if (isArray(workspace)) {
|
||||
for (let c of workspace) {
|
||||
workspaceStore.put(c);
|
||||
}
|
||||
} else {
|
||||
workspaceStore.put(workspace);
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
tx.oncomplete = () => res();
|
||||
tx.onerror = () => rej(tx.error);
|
||||
});
|
||||
})
|
||||
.then(resolve)
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteWorkspaceInIdb = (connection, workspaceUid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection
|
||||
.then((db) => {
|
||||
let tx = db.transaction(`workspace`, 'readwrite');
|
||||
tx.objectStore('workspace').delete(workspaceUid);
|
||||
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkspacesFromIdb = (connection) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection
|
||||
.then((db) => {
|
||||
let tx = db.transaction('workspace');
|
||||
let workspaceStore = tx.objectStore('workspace');
|
||||
return workspaceStore.getAll();
|
||||
})
|
||||
.then((workspaces) => {
|
||||
if (!Array.isArray(workspaces)) {
|
||||
return new Error('IDB Corrupted');
|
||||
}
|
||||
|
||||
return resolve(workspaces);
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
import fileDialog from 'file-dialog';
|
||||
import { saveCollectionToIdb } from 'utils/idb';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, updateUidsInCollection } from './common';
|
||||
import sampleCollection from './samples/sample-collection.json';
|
||||
import { validateSchema, updateUidsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -30,10 +28,9 @@ const importCollection = () => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parseJsonCollection)
|
||||
.then(validateSchema)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(updateUidsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||
.then((collection) => resolve(collection))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
@@ -42,15 +39,4 @@ const importCollection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const importSampleCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
validateSchema(sampleCollection)
|
||||
.then(updateUidsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
||||
|
||||
@@ -4,6 +4,7 @@ import get from 'lodash/get';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import { isItemARequest } from 'utils/collections';
|
||||
import { collectionSchema } from '@usebruno/schema';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
|
||||
@@ -40,5 +41,32 @@ export const updateUidsInCollection = (_collection) => {
|
||||
};
|
||||
updateItemUids(collection.items);
|
||||
|
||||
const updateEnvUids = (envs = []) => {
|
||||
each(envs, (env) => {
|
||||
env.uid = uuid();
|
||||
each(env.variables, (variable) => (variable.uid = uuid()));
|
||||
});
|
||||
};
|
||||
updateEnvUids(collection.environments);
|
||||
updateEnvUids(collection.environments);
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
export const hydrateSeqInCollection = (collection) => {
|
||||
const hydrateSeq = (items = []) => {
|
||||
let index = 1;
|
||||
each(items, (item) => {
|
||||
if(isItemARequest(item) && !item.seq) {
|
||||
item.seq = index;
|
||||
index++;
|
||||
}
|
||||
if (item.items && item.items.length) {
|
||||
hydrateSeq(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
hydrateSeq(collection.items);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { saveCollectionToIdb } from 'utils/idb';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, updateUidsInCollection } from './common';
|
||||
import { validateSchema, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -179,10 +178,8 @@ const importCollection = () => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parsePostmanCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then(updateUidsInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => saveCollectionToIdb(window.__idb, collection))
|
||||
.then((collection) => resolve(collection))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user