Compare commits

..

91 Commits

Author SHA1 Message Date
Anoop M D
667811cbd4 bumped version 2023-01-29 20:45:30 +05:30
Anoop M D
7839e93a57 fix: fixed dark mode issues 2023-01-29 20:44:34 +05:30
Anoop M D
11c60273b4 fix: fixed missing module error 2023-01-29 19:52:13 +05:30
Anoop M D
1dcff56c78 release: v0.7.0 2023-01-29 19:27:44 +05:30
Anoop M D
2e32423869 feat: better error messaging 2023-01-29 18:04:17 +05:30
Anoop M D
c328281f21 feat: testing support has arrived ! 2023-01-29 17:35:28 +05:30
Anoop M D
cc261326fc fix: fixed env var issues 2023-01-29 14:17:01 +05:30
Anoop M D
050ee2680f feat: improved request queuing status functionality 2023-01-29 13:20:19 +05:30
Anoop M D
b2c28465e9 fix: patch bug temporarily 2023-01-29 13:09:33 +05:30
Anoop M D
cd36335c60 feat: support crypto-js as an inbuilt library 2023-01-29 12:34:37 +05:30
Anoop M D
d89f12c071 feat: support loading external libraries 2023-01-29 12:33:12 +05:30
Anoop M D
905f459ed0 feat: support for inbuilt libraries during scripting 2023-01-29 11:12:34 +05:30
Anoop M D
b800055df4 chore: hiding menubar icon temporarily 2023-01-29 11:12:11 +05:30
Anoop M D
b1d2b798ba feat: scripting support almost done 2023-01-29 04:49:31 +05:30
Anoop M D
4a403a253e feat: show current env vars 2023-01-27 03:24:21 +05:30
Anoop M D
a45628dd85 feat: moved env var interpolation logic to electron 2023-01-25 10:39:07 +05:30
Anoop M D
977637e556 chore: updated manifesto in readme 2023-01-24 21:44:33 +05:30
Anoop M D
3d63db806d feat: moved prepare request logic to electron 2023-01-24 19:34:06 +05:30
Anoop M D
1ec24d1138 feat: scripting support (#16) 2023-01-24 18:27:47 +05:30
Anoop M D
fa40685a6a feat: skipping telemetry in dev env 2023-01-24 17:28:31 +05:30
Anoop M D
037013005f chore: bumped version 2023-01-23 00:18:54 +05:30
Anoop M D
0c42298ce6 feat: deprecated form-url-encoded in favour of form-urlencoded in body type in bru lang 2023-01-22 23:49:25 +05:30
Anoop M D
84ce75263b chore: added bruno-schema tests to github workflow 2023-01-22 23:39:59 +05:30
Anoop M D
5c8d0a9e8a feat: script and tests functionality 2023-01-22 23:39:16 +05:30
Anoop M D
b70bbf78b1 chore: renamed file 2023-01-22 18:35:57 +05:30
Anoop M D
43b9412ddb chore: rename package 2023-01-22 18:32:28 +05:30
Anoop M D
b56972fd93 chore: updated readme 2023-01-22 06:02:00 +05:30
Anoop M D
c102ac527a chore: updated readme 2023-01-22 06:00:00 +05:30
Anoop M D
45229b1af7 chore: version bump 2023-01-22 02:50:24 +05:30
Anoop M D
f9a3fb2f1b chore: cleanup old files 2023-01-22 02:43:14 +05:30
Anoop M D
65d8a707d8 chore: updated readme 2023-01-22 02:41:31 +05:30
Anoop M D
cc6bf45d5f chore: updated dev docs 2023-01-22 02:38:59 +05:30
Anoop M D
8fbb777665 feat: ask foldername when creating collection 2023-01-22 02:34:23 +05:30
Anoop M D
0e041d460c feat: listeners for cmd+s ctr+s in SingleLineEditor 2023-01-22 00:49:11 +05:30
Anoop M D
2e3b296021 feat: making regex in bruno lang support windows line endings 2023-01-22 00:38:10 +05:30
Anoop M D
405b50edcd fix: support parsing of empty urls in bru files 2023-01-22 00:35:58 +05:30
Anoop M D
fff540010e feat: hardening seq number functionality 2023-01-21 23:22:45 +05:30
Anoop M D
e513694912 fix: fixed seq type bug in bruno lang parser 2023-01-21 19:56:39 +05:30
Anoop M D
d01cada16c fix: fix json stringify bug in response timeline 2023-01-21 19:54:06 +05:30
Anoop M D
dd4fecfd1c fix: fix env info popup issues in graphql query editor 2023-01-21 19:41:37 +05:30
Anoop M D
095d7c6bcb feat: dark mode styles for env var info popup 2023-01-21 19:07:58 +05:30
Anoop M D
d165a04377 feat: environment variable syntax highlighting 2023-01-21 18:12:34 +05:30
Anoop M D
d3d1e47950 fix: fixed unit tests 2023-01-21 01:56:30 +05:30
Anoop M D
9c14941c15 feat: using single line editors instead of input boxes 2023-01-21 01:42:20 +05:30
Anoop M D
1627f65bd7 feat: using single line editors instead of input boxes 2023-01-21 01:38:48 +05:30
Anoop M D
19f4f3c1a5 chore: deprecating descriptions temporarily in key value pairs 2023-01-21 01:26:40 +05:30
Anoop M D
ae70680ceb feat: ener keybindings for single line editor 2023-01-21 01:23:33 +05:30
Anoop M D
60fc13c765 feat: refactor codemirror bruno variables mode 2023-01-21 01:17:27 +05:30
Anoop M D
60c3d41c8e feat: codemirror single line editor 2023-01-20 09:39:32 +05:30
Anoop M D
fb8ff37d83 feat: codemirror syntax highlight for env vars 2023-01-20 08:14:03 +05:30
Anoop M D
0d9b30e730 chore: updated .bru examples 2023-01-20 07:23:31 +05:30
Anoop M D
695f42df80 feat: run request upon cmd+enter from response pane 2023-01-20 07:16:57 +05:30
Anoop M D
6b43159be2 chore: cleanup 2023-01-20 03:27:01 +05:30
Anoop M D
21c9c8b4fb feat: drag and drop for files and folders 2023-01-20 00:45:07 +05:30
Anoop M D
c4abe54c3f fix: disable watcher updates on env directories 2023-01-19 02:00:02 +05:30
Anoop M D
dd71c9e71b feat: response timeline 2023-01-18 20:55:10 +05:30
Anoop M D
2be3e4bf69 feat: yay node v14 to v18 2023-01-18 20:53:27 +05:30
Anoop M D
f34e9f7b26 chore: fixed typo 2023-01-18 14:47:43 +05:30
Anoop M D
76b0729af3 feat: ditched web, all in on desktop app 2023-01-18 04:11:42 +05:30
Anoop M D
4877bc3849 chore: updated product tagline 2023-01-17 19:42:34 +05:30
Anoop M D
0742e3415c feat: github workflow for running unit tests 2023-01-17 19:35:59 +05:30
Anoop M D
ae7e3a722c chore: disable dragndrop temporarily due to electron issues 2023-01-17 19:27:11 +05:30
Anoop M D
7f2e19250f feat: generate collection hash on the fly 2023-01-17 19:26:41 +05:30
Anoop M D
4e16e954ef Merge branch 'main' of github.com:usebruno/bruno 2023-01-17 02:01:36 +05:30
Anoop M D
b6c3205474 feat: integrating app with the bru lang 2023-01-17 02:00:58 +05:30
Anoop M D
23076b41c6 feat: bruno land outdent strings during parsing 2023-01-17 00:55:47 +05:30
Anoop M D
b5116b54af feat: fix bugs in bruno-lang in data format 2023-01-17 00:20:22 +05:30
A-childs-encyclopedia
83aaa21b5b Update readme.md (#77) 2023-01-16 09:23:28 +05:30
Anoop M D
e1e7b37ce5 feat: bruno lang support for stringify json into bru file 2023-01-16 00:49:06 +05:30
Anoop M D
8dab9268f2 feat: bruno lang now supports parsing text only multipart form data 2023-01-15 23:02:59 +05:30
Anoop M D
4eed999db1 feat: bruno lang now supports parsing for url encoded params 2023-01-15 22:42:56 +05:30
Anoop M D
c29ab50a3d chore: bruno lang improve parsing logic of headers and params 2023-01-15 05:04:35 +05:30
Anoop M D
5e1d6cba4a feat: bruno lang support for parsing xml body 2023-01-15 05:03:58 +05:30
Anoop M D
a645d1459c feat: bruno lang - support parsing text body 2023-01-15 04:03:52 +05:30
Anoop M D
24e11a864c feat: bruno lang - parse graphql body 2023-01-15 03:51:48 +05:30
Anoop M D
87a4778a91 feat: bruno-lang now supprts parsing body json 2023-01-15 00:45:01 +05:30
Anoop M D
0750af4c68 chore: added examples inside bruno-lang 2023-01-15 00:44:21 +05:30
Anoop M D
60e613fac8 feat: bruno lang support parsing headers in .bru file 2023-01-14 20:21:54 +05:30
Anoop M D
b75baf57ba feat: bruno lang parse .bru file 2023-01-14 20:16:09 +05:30
Anoop M D
137df3c5c0 feat: bruno lang inline tag parser 2023-01-14 16:53:52 +05:30
Anoop M D
6ef2daebbd chore: updated bru files formatting 2023-01-14 04:37:39 +05:30
Anoop M D
55f85e3728 chore: updated bru files 2023-01-14 04:21:57 +05:30
Anoop M D
f0269069d2 feat: brun-lang package init 2023-01-12 23:46:01 +05:30
Anoop M D
61dbca3243 feat: highlight js inside script block in .bru files 2023-01-10 10:58:47 +05:30
Anoop M D
f21cb240c4 feat: vscode extension for bruno 2023-01-10 10:07:30 +05:30
Anoop M D
ca46e14732 feat: bru-file package init 2023-01-10 09:45:24 +05:30
Anoop M D
87f6000b85 Merge branch 'feature/sort-requests' 2023-01-10 09:39:13 +05:30
Anoop M D
36d0550472 feat: drag item to root of collection 2023-01-10 09:37:09 +05:30
Anoop M D
ee4734c957 feat: move requests across folders 2022-12-28 04:48:49 +05:30
Anoop M D
02f9fc0a7b fix: fixed mac electron build issues 2022-12-22 00:34:10 +05:30
Anoop M D
6ce657d891 feat: drag and drop events (#57) 2022-11-11 03:53:51 +05:30
186 changed files with 5503 additions and 3661 deletions

21
.github/workflows/unit-tests.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
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
- name: Test Package bruno-schema
run: npm run test --workspace=packages/bruno-schema

2
.nvmrc
View File

@@ -1 +1 @@
v14.18.0
v18.13.0

BIN
assets/images/landing-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

View File

@@ -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

View File

@@ -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
```

View File

@@ -6,6 +6,8 @@
"packages/bruno-electron",
"packages/bruno-tauri",
"packages/bruno-schema",
"packages/bruno-js",
"packages/bruno-lang",
"packages/bruno-testbench",
"packages/bruno-graphql-docs"
],

View File

@@ -1,8 +1,9 @@
module.exports = {
reactStrictMode: true,
reactStrictMode: false,
publicRuntimeConfig: {
CI: process.env.CI,
PLAYWRIGHT: process.env.PLAYWRIGHT
PLAYWRIGHT: process.env.PLAYWRIGHT,
ENV: process.env.ENV
},
webpack: (config, { isServer }) => {
// Fixes npm packages that depend on `fs` module

View File

@@ -1,8 +1,9 @@
{
"name": "@usebruno/app",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "cross-env ENV=dev next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint",
@@ -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,17 +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",
"reckonjs": "^0.1.2",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"split-on-first": "^3.0.0",
"styled-components": "^5.3.3",
@@ -58,6 +60,7 @@
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.3",
"babel-loader": "^8.2.3",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"file-loader": "^6.2.0",
"html-loader": "^3.0.1",

View File

@@ -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;

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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' }}>

View File

@@ -30,6 +30,7 @@ const Wrapper = styled.div`
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;

View File

@@ -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>

View File

@@ -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}

View File

@@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
import RequestHeaders from 'components/RequestPane/RequestHeaders';
import RequestBody from 'components/RequestPane/RequestBody';
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
import Script from 'components/RequestPane/Script';
import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
@@ -34,6 +36,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
case 'headers': {
return <RequestHeaders item={item} collection={collection} />;
}
case 'script': {
return <Script item={item} collection={collection} />;
}
case 'tests': {
return <Tests item={item} collection={collection} />;
}
default: {
return <div className="mt-4">404 | Not found</div>;
}
@@ -67,6 +75,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
</div>
{/* Moved to post mvp */}
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
{focusedTab.requestPaneTab === 'body' ? (

View File

@@ -30,6 +30,8 @@ const Wrapper = styled.div`
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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

View File

@@ -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>

View File

@@ -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}/>

View File

@@ -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>
);
}

View File

@@ -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>

View File

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

View File

@@ -0,0 +1,45 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestScript } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Script = ({ item, collection }) => {
const dispatch = useDispatch();
const script = item.draft ? get(item, 'draft.request.script') : get(item, 'request.script');
const {
storedTheme
} = useTheme();
const onEdit = (value) => {
dispatch(
updateRequestScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full">
<CodeEditor
collection={collection} value={script || ''}
theme={storedTheme}
onEdit={onEdit}
mode='javascript'
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};
export default Script;

View File

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

View File

@@ -0,0 +1,45 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ item, collection }) => {
const dispatch = useDispatch();
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
const {
storedTheme
} = useTheme();
const onEdit = (value) => {
dispatch(
updateRequestTests({
tests: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full">
<CodeEditor
collection={collection} value={tests || ''}
theme={storedTheme}
onEdit={onEdit}
mode='javascript'
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};
export default Tests;

View File

@@ -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

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { IconFiles } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import VariablesView from 'components/VariablesView';
import StyledWrapper from './StyledWrapper';
const CollectionToolBar = ({ collection }) => {
@@ -12,6 +13,7 @@ const CollectionToolBar = ({ collection }) => {
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<VariablesView collection={collection}/>
<EnvironmentSelector collection={collection} />
</div>
</div>

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -25,6 +25,14 @@ const StyledWrapper = styled.div`
}
}
}
.some-tests-failed {
color: ${(props) => props.theme.colors.text.danger} !important;
}
.all-tests-passed {
color: ${(props) => props.theme.colors.text.green} !important;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,17 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.test-success {
color: ${(props) => props.theme.colors.text.green};
}
.test-failure {
color: ${(props) => props.theme.colors.text.danger};
.error-message {
color: ${(props) => props.theme.colors.text.muted};
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const TestResults = ({ results }) => {
if (!results || !results.length) {
return (
<div className="px-3">
No tests found
</div>
);
}
const passedTests = results.filter((result) => result.status === 'pass');
const failedTests = results.filter((result) => result.status === 'fail');
return (
<StyledWrapper className='flex flex-col px-3'>
<div className="py-2 font-medium test-summary">
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
</div>
<ul className="">
{results.map((result) => (
<li key={result.uid} className="py-1">
{result.status === 'pass' ? (
<span className="test-success">
&#x2714;&nbsp; {result.description}
</span>
) : (
<>
<span className="test-failure">
&#x2718;&nbsp; {result.description}
</span>
<br />
<span className="error-message pl-8">
{result.error}
</span>
</>
)}
</li>
))}
</ul>
</StyledWrapper>
);
};
export default TestResults;

View File

@@ -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;

View File

@@ -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;

View File

@@ -10,13 +10,39 @@ import ResponseHeaders from './ResponseHeaders';
import StatusCode from './StatusCode';
import ResponseTime from './ResponseTime';
import ResponseSize from './ResponseSize';
import Timeline from './Timeline';
import TestResults from './TestResults';
import StyledWrapper from './StyledWrapper';
const TestResultsLabel = ({ results }) => {
if(!results || !results.length) {
return 'Tests';
}
const numberOfTests = results.length;
const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
return (
<div className='flex items-center'>
<div>Tests</div>
{numberOfFailedTests ? (
<sup className='sups some-tests-failed ml-1 font-medium'>
{numberOfFailedTests}
</sup>
) : (
<sup className='sups all-tests-passed ml-1 font-medium'>
{numberOfTests}
</sup>
)}
</div>
);
};
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isLoading = item.response && item.response.state === 'sending';
const isLoading = ['queued', 'sending'].includes(item.requestState);
const selectTab = (tab) => {
dispatch(
@@ -32,11 +58,22 @@ 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} />;
}
case 'tests': {
return <TestResults results={item.testResults} />;
}
default: {
return <div>404 | Not found</div>;
@@ -84,6 +121,12 @@ 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>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
<TestResultsLabel results={item.testResults} />
</div>
{!isLoading ? (
<div className="flex flex-grow justify-end items-center">
<StatusCode status={response.status} />

View File

@@ -70,7 +70,7 @@ const Wrapper = styled.div`
}
}
&.is-dragging .collection-item-name {
&.is-sidebar-dragging .collection-item-name {
cursor: inherit;
}
`;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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'));
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -27,18 +27,12 @@ const MenuBar = () => {
{openTheme && <SwitchTheme onClose={() => setOpenTheme(false)} />}
<div className="flex flex-col">
<Link href="/">
{/* Todo: Fix this: Clicking on this crashes the app */}
{/* <Link href="/">
<div className={getClassName('/')}>
<IconCode size={28} strokeWidth={1.5} />
</div>
</Link>
{!isPlatformElectron ? (
<Link href="/collections">
<div className={getClassName('/collections')}>
<IconFiles size={28} strokeWidth={1.5} />
</div>
</Link>
) : null}
</Link> */}
{/* <div className="menu-item">
<IconUsers size={28} strokeWidth={1.5}/>
</div> */}

View File

@@ -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};

View File

@@ -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>

View File

@@ -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.7.2</div>
</div>
</div>
</div>

View File

@@ -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;

View File

@@ -0,0 +1,103 @@
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': () => {
if (this.props.onSave) {
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;

View 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;

View File

@@ -0,0 +1,19 @@
import styled from 'styled-components';
const Wrapper = styled.div`
position: absolute;
min-width: fit-content;
font-size: 14px;
top: 36px;
right: 0;
white-space: nowrap;
z-index: 1000;
background-color: ${(props) => props.theme.variables.bg};
.popover {
border-radius: 2px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
`;
export default Wrapper;

View File

@@ -0,0 +1,30 @@
import React, {useRef} from 'react';
import StyledWrapper from './StyledWrapper';
import useOnClickOutside from 'hooks/useOnClickOutside';
const PopOver = ({
children,
iconRef,
handleClose
}) => {
const popOverRef = useRef(null);
useOnClickOutside(popOverRef, (e) => {
if(iconRef && iconRef.current) {
if (e.target == iconRef.current || iconRef.current.contains(e.target)) {
return;
}
}
handleClose();
});
return (
<StyledWrapper>
<div className="popover" ref={popOverRef}>
<div className="popover-content">{children}</div>
</div>
</StyledWrapper>
);
};
export default PopOver;

View File

@@ -0,0 +1,15 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
position: relative;
align-self: stretch;
display: flex;
align-items: center;
.view-environment {
width: 1rem;
font-size: 10px;
}
`
export default StyledWrapper;

View File

@@ -0,0 +1,19 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.variable-name {
color: ${(props) => props.theme.variables.name.color};
}
.variable-name{
width:100px;
}
.variable-value {
max-width: 500px;
inline-size: 500px;
overflow-wrap: break-word;
}
`
export default StyledWrapper;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const VariablesTable = ({ variables }) => {
return (
<StyledWrapper>
<div className="flex flex-col w-full">
{(variables && variables.length) ? variables.map((variable) => {
return (
<div key={variable.uid} className="flex">
<div className='variable-name text-yellow-600 text-right pr-2'>{variable.name}</div>
<div className='variable-value pl-2 whitespace-normal text-left flex-grow'>{variable.value}</div>
</div>
);
}) : null}
</div>
</StyledWrapper>
);
};
export default VariablesTable;

View File

@@ -0,0 +1,46 @@
import React, { useState, useRef } from 'react';
import get from 'lodash/get';
import filter from 'lodash/filter';
import { findEnvironmentInCollection } from 'utils/collections';
import VariablesTable from './VariablesTable';
import StyledWrapper from './StyledWrapper';
import PopOver from './PopOver';
import { IconEye } from '@tabler/icons';
const VariablesView = ({collection}) => {
const iconRef = useRef(null);
const [popOverOpen, setPopOverOpen] = useState(false);
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
const variables = get(environment, 'variables', []);
const enabledVariables = filter(variables, (variable) => variable.enabled);
return (
<StyledWrapper
className="mr-2 server-syncstatus-icon"
ref={iconRef}
>
<div className="flex p-1 items-center"
onClick={() => setPopOverOpen(true)}
onMouseEnter={() => setPopOverOpen(true)}
onMouseLeave={() => setPopOverOpen(false)}
>
<div className='cursor-pointer view-environment'>
<IconEye size={18} strokeWidth={1.5} />
</div>
{popOverOpen && (
<PopOver
iconRef={iconRef}
handleClose={() => setPopOverOpen(false)}
>
<div className="px-2 py-1">
{(enabledVariables && enabledVariables.length) ? <VariablesTable variables={enabledVariables} /> : 'No variables found'}
</div>
</PopOver>
)}
</div>
</StyledWrapper>
)
};
export default VariablesView;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,34 @@
// See https://usehooks.com/useOnClickOutside/
import { useEffect } from 'react';
const useOnClickOutside = (ref, handler) => {
useEffect(
() => {
const listener = event => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[ref, handler]
);
};
export default useOnClickOutside;

View File

@@ -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>
</>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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();

Some files were not shown because too many files have changed in this diff Show More