feat: ditched web, all in on desktop app

This commit is contained in:
Anoop M D 2023-01-18 04:11:42 +05:30
parent 4877bc3849
commit 76b0729af3
73 changed files with 981 additions and 2830 deletions

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

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

@ -11,23 +11,19 @@ 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();
@ -71,8 +67,6 @@ const Collection = ({ collection, searchText }) => {
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
};
const isLocal = isLocalCollection(collection);
// const [{ isOver }, drop] = useDrop({
// accept: 'COLLECTION_ITEM',
// drop: (draggedItem) => {
@ -93,9 +87,7 @@ const Collection = ({ collection, searchText }) => {
{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)} />}
{showRemoveCollectionModal && <RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />}
<div className="flex py-1 collection-name items-center">
<div className="flex flex-grow items-center" onClick={handleClick}>
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, color: 'rgb(160 160 160)' }} />
@ -121,8 +113,8 @@ const Collection = ({ collection, searchText }) => {
>
New Folder
</div>
{!isLocal ? (
<div
{/* Todo: implement rename collection */}
{/* <div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
@ -130,8 +122,7 @@ const Collection = ({ collection, searchText }) => {
}}
>
Rename
</div>
) : null}
</div> */}
<div
className="dropdown-item"
onClick={(e) => {
@ -141,38 +132,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);
setShowRemoveCollectionModal(true);
}}
>
Remove
</div>
)}
{!isLocal ? (
<div
className="dropdown-item delete-collection"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setShowDeleteCollectionModal(true);
}}
>
Delete
</div>
) : null}
</Dropdown>
</div>
</div>

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,42 +1,80 @@
import React from 'react';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
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';
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 CollectionsBadge = () => {
return (
<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 { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
const activeWorkspace = find(workspaces, (w) => w.uid === activeWorkspaceUid);
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
if (!activeWorkspace) {
return null;
}
const collectionToDisplay = filter(collections, (c) => findCollectionInWorkspace(activeWorkspace, c.uid) && !isLocalCollection(c));
if (!collectionToDisplay || !collectionToDisplay.length) {
return <CreateOrAddCollection />;
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">
{collectionToDisplay && collectionToDisplay.length
? collectionToDisplay.map((c) => {
{collections && collections.length
? collections.map((c) => {
return (
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} />
<Collection searchText={searchText} collection={c} key={c.uid} />
</DndProvider>
);
})
: null}
</div>
</StyledWrapper>
);
};
export default Collections;

View File

@ -2,16 +2,14 @@ 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 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,
@ -23,8 +21,7 @@ const CreateCollection = ({ onClose, isLocal }) => {
collectionName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
}),
onSubmit: (values) => {
const action = isLocal && isPlatformElectron ? createLocalCollection : createCollection;
dispatch(action(values.collectionName, values.collectionLocation))
dispatch(createCollection(values.collectionName, values.collectionLocation))
.then(() => {
toast.success('Collection created');
onClose();
@ -34,7 +31,7 @@ const CreateCollection = ({ onClose, isLocal }) => {
});
const browse = () => {
dispatch(browserLocalDirectory())
dispatch(browseDirectory())
.then((dirPath) => {
formik.setFieldValue('collectionLocation', dirPath);
})
@ -74,7 +71,6 @@ 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
@ -93,18 +89,15 @@ const CreateCollection = ({ onClose, isLocal }) => {
onClick={browse}
/>
</>
) : null}
{isLocal && isPlatformElectron && formik.touched.collectionLocation && formik.errors.collectionLocation ? (
{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>
</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

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

View File

@ -1,61 +1,50 @@
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} />
@ -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
<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

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

@ -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 IDE for exploring and testing api's.</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 IDE for exploring and testing api's.</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,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();

View File

@ -1,17 +1,18 @@
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import {
localCollectionAddDirectoryEvent,
localCollectionAddFileEvent,
localCollectionChangeFileEvent,
localCollectionUnlinkFileEvent,
localCollectionUnlinkDirectoryEvent
collectionAddDirectoryEvent,
collectionAddFileEvent,
collectionChangeFileEvent,
collectionUnlinkFileEvent,
collectionUnlinkDirectoryEvent,
collectionUnlinkEnvFileEvent
} from 'providers/ReduxStore/slices/collections';
import toast from 'react-hot-toast';
import { openLocalCollectionEvent, localCollectionLoadEnvironmentsEvent } from 'providers/ReduxStore/slices/collections/actions';
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
import { isElectron } from 'utils/common/platform';
const useLocalCollectionTreeSync = () => {
const useCollectionTreeSync = () => {
const dispatch = useDispatch();
useEffect(() => {
@ -23,27 +24,27 @@ const useLocalCollectionTreeSync = () => {
const _openCollection = (pathname, uid, name) => {
console.log(`collection uid: ${uid}, pathname: ${pathname}, name: ${name}`);
dispatch(openLocalCollectionEvent(uid, pathname, name));
dispatch(openCollectionEvent(uid, pathname, name));
};
const _collectionTreeUpdated = (type, val) => {
if (type === 'addDir') {
dispatch(
localCollectionAddDirectoryEvent({
collectionAddDirectoryEvent({
dir: val
})
);
}
if (type === 'addFile') {
dispatch(
localCollectionAddFileEvent({
collectionAddFileEvent({
file: val
})
);
}
if (type === 'change') {
dispatch(
localCollectionChangeFileEvent({
collectionChangeFileEvent({
file: val
})
);
@ -51,7 +52,7 @@ const useLocalCollectionTreeSync = () => {
if (type === 'unlink') {
setTimeout(() => {
dispatch(
localCollectionUnlinkFileEvent({
collectionUnlinkFileEvent({
file: val
})
);
@ -59,21 +60,21 @@ const useLocalCollectionTreeSync = () => {
}
if (type === 'unlinkDir') {
dispatch(
localCollectionUnlinkDirectoryEvent({
collectionUnlinkDirectoryEvent({
directory: val
})
);
}
if (type === 'addEnvironmentFile') {
dispatch(localCollectionLoadEnvironmentsEvent(val));
dispatch(collectionAddEnvFileEvent(val));
}
if (type === 'changeEnvironmentFile') {
dispatch(localCollectionLoadEnvironmentsEvent(val));
if (type === 'unlinkEnvironmentFile') {
dispatch(collectionUnlinkEnvFileEvent(val));
}
};
const _collectionAlreadyOpened = (pathname) => {
toast.success('Collection is already opened under local collections');
toast.success('Collection is already opened');
};
const _displayError = (message) => {
@ -96,4 +97,4 @@ const useLocalCollectionTreeSync = () => {
}, [isElectron]);
};
export default useLocalCollectionTreeSync;
export default useCollectionTreeSync;

View File

@ -1,34 +0,0 @@
import { useEffect } from 'react';
import { openDB } from 'idb';
import { idbConnectionReady } from 'providers/ReduxStore/slices/app';
import { loadCollectionsFromIdb } from 'providers/ReduxStore/slices/collections/actions';
import { loadWorkspacesFromIdb } from 'providers/ReduxStore/slices/workspaces/actions';
import { useDispatch } from 'react-redux';
const useIdb = () => {
const dispatch = useDispatch();
useEffect(() => {
let dbName = `bruno`;
let connection = openDB(dbName, 1, {
upgrade(db, oldVersion, newVersion, transaction) {
switch (oldVersion) {
case 0:
const collectionStore = db.createObjectStore('collection', { keyPath: 'uid' });
const workspaceStore = db.createObjectStore('workspace', { keyPath: 'uid' });
}
}
});
connection
.then(() => {
window.__idb = connection;
dispatch(idbConnectionReady());
dispatch(loadCollectionsFromIdb());
dispatch(loadWorkspacesFromIdb());
})
.catch((err) => console.log(err));
}, []);
};
export default useIdb;

View File

@ -2,14 +2,12 @@ import { configureStore } from '@reduxjs/toolkit';
import appReducer from './slices/app';
import collectionsReducer from './slices/collections';
import tabsReducer from './slices/tabs';
import workspacesReducer from './slices/workspaces';
export const store = configureStore({
reducer: {
app: appReducer,
collections: collectionsReducer,
tabs: tabsReducer,
workspaces: workspacesReducer
tabs: tabsReducer
}
});

View File

@ -1,8 +1,6 @@
import path from 'path';
import filter from 'lodash/filter';
import each from 'lodash/each';
import trim from 'lodash/trim';
import toast from 'react-hot-toast';
import { uuid } from 'utils/common';
import cloneDeep from 'lodash/cloneDeep';
import {
@ -13,21 +11,18 @@ import {
recursivelyGetAllItemUids,
transformCollectionToSaveToIdb,
transformRequestToSaveToFilesystem,
deleteItemInCollection,
findParentItemInCollection,
findEnvironmentInCollection,
isItemAFolder,
refreshUidsInItem,
interpolateEnvironmentVars,
getDefaultRequestPaneTab
interpolateEnvironmentVars
} from 'utils/collections';
import { collectionSchema, itemSchema, environmentsSchema } from '@usebruno/schema';
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
import { waitForNextTick } from 'utils/common';
import { getCollectionsFromIdb, saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
import { saveCollectionToIdb } from 'utils/idb';
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
import {
loadCollections,
requestSent,
requestCancelled,
responseReceived,
@ -38,91 +33,18 @@ import {
moveItem as _moveItem,
moveItemToRootOfCollection as _moveItemToRootOfCollection,
saveRequest as _saveRequest,
addEnvironment as _addEnvironment,
renameEnvironment as _renameEnvironment,
deleteEnvironment as _deleteEnvironment,
saveEnvironment as _saveEnvironment,
selectEnvironment as _selectEnvironment,
createCollection as _createCollection,
renameCollection as _renameCollection,
deleteCollection as _deleteCollection,
localCollectionLoadEnvironmentsEvent as _localCollectionLoadEnvironmentsEvent
removeCollection as _removeCollection,
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
} from './index';
import { closeTabs, addTab } from 'providers/ReduxStore/slices/tabs';
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { isLocalCollection, resolveRequestFilename } from 'utils/common/platform';
const PATH_SEPARATOR = path.sep;
export const loadCollectionsFromIdb = () => (dispatch) => {
getCollectionsFromIdb(window.__idb)
.then((collections) =>
dispatch(
loadCollections({
collections: collections
})
)
)
.catch(() => toast.error('Error occured while loading collections from IndexedDB'));
};
export const createCollection = (collectionName) => (dispatch, getState) => {
const newCollection = {
version: '1',
uid: uuid(),
name: collectionName,
items: [],
environments: []
};
const requestItem = {
uid: uuid(),
type: 'http-request',
name: 'Untitled',
request: {
method: 'GET',
url: '',
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
multipartForm: null,
formUrlEncoded: null
}
}
};
newCollection.items.push(requestItem);
const state = getState();
const { activeWorkspaceUid } = state.workspaces;
return new Promise((resolve, reject) => {
collectionSchema
.validate(newCollection)
.then(() => saveCollectionToIdb(window.__idb, newCollection))
.then(() => dispatch(_createCollection(newCollection)))
.then(waitForNextTick)
.then(() => dispatch(addCollectionToWorkspace(activeWorkspaceUid, newCollection.uid)))
.then(waitForNextTick)
.then(() =>
dispatch(
addTab({
uid: requestItem.uid,
collectionUid: newCollection.uid,
requestPaneTab: getDefaultRequestPaneTab(requestItem)
})
)
)
.then(resolve)
.catch(reject);
});
};
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@ -149,33 +71,6 @@ export const renameCollection = (newName, collectionUid) => (dispatch, getState)
}
};
export const deleteCollection = (collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
if (!collection) {
return reject('collection not found');
}
deleteCollectionInIdb(window.__idb, collection.uid)
.then(() => {
dispatch(
closeTabs({
tabUids: recursivelyGetAllItemUids(collection.items)
})
);
dispatch(
_deleteCollection({
collectionUid: collectionUid
})
);
})
.then(resolve)
.catch(reject);
});
};
export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@ -186,10 +81,11 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
}
const collectionCopy = cloneDeep(collection);
if (isLocalCollection(collection)) {
const item = findItemInCollection(collectionCopy, itemUid);
if (item) {
if (!item) {
return reject(new Error('Not able to locate item'));
}
const itemToSave = transformRequestToSaveToFilesystem(item);
const { ipcRenderer } = window;
@ -198,27 +94,6 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
.then(resolve)
.catch(reject);
} else {
reject(new Error('Not able to locate item'));
}
return;
}
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_saveRequest({
itemUid: itemUid,
collectionUid: collectionUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
});
};
@ -298,7 +173,6 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
return reject(new Error('Collection not found'));
}
if (isLocalCollection(collection)) {
if (!itemUid) {
const folderWithSameNameExists = find(collection.items, (i) => i.type === 'folder' && trim(i.name) === trim(folderName));
if (!folderWithSameNameExists) {
@ -331,41 +205,6 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
return reject(new Error('unable to find parent folder'));
}
}
return;
}
const collectionCopy = cloneDeep(collection);
const item = {
uid: uuid(),
name: folderName,
type: 'folder',
items: []
};
if (!itemUid) {
collectionCopy.items.push(item);
} else {
const currentItem = findItemInCollection(collectionCopy, itemUid);
if (currentItem && currentItem.type === 'folder') {
currentItem.items = currentItem.items || [];
currentItem.items.push(item);
}
}
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_newItem({
item: item,
currentItemUid: itemUid,
collectionUid: collectionUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
});
};
@ -384,7 +223,6 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
return reject(new Error('Unable to locate item'));
}
if (isLocalCollection(collection)) {
const dirname = path.dirname(item.pathname);
let newPathname = '';
@ -396,30 +234,10 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
}
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
return;
}
item.name = newName;
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy, {
ignoreDraft: true
});
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_renameItem({
newName: newName,
itemUid: itemUid,
collectionUid: collectionUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
ipcRenderer
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
.then(resolve)
.catch(reject);
});
};
@ -441,7 +259,6 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
throw new Error('Cloning folders is not supported yet');
}
if (isLocalCollection(collection)) {
const parentItem = findParentItemInCollection(collectionCopy, itemUid);
const filename = resolveRequestFilename(newName);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(item));
@ -476,39 +293,6 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
return reject(new Error(`${requestName} already exists in the folder`));
}
}
return;
}
// todo: clone query params
const clonedItem = cloneDeep(item);
clonedItem.name = newName;
clonedItem.uid = uuid();
each(clonedItem.headers, (h) => (h.uid = uuid()));
const parentItem = findParentItemInCollection(collectionCopy, itemUid);
if (!parentItem) {
collectionCopy.items.push(clonedItem);
} else {
parentItem.items.push(clonedItem);
}
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_cloneItem({
parentItemUid: parentItem ? parentItem.uid : null,
clonedItem: clonedItem,
collectionUid: collectionUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
});
};
@ -521,7 +305,6 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
return reject(new Error('Collection not found'));
}
if (isLocalCollection(collection)) {
const item = findItemInCollection(collection, itemUid);
if (item) {
const { ipcRenderer } = window;
@ -532,25 +315,6 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
.catch((error) => reject(error));
}
return;
}
const collectionCopy = cloneDeep(collection);
deleteItemInCollection(itemUid, collectionCopy);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_deleteItem({
itemUid: itemUid,
collectionUid: collectionUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
});
};
@ -699,7 +463,6 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
}
};
if (isLocalCollection(collection)) {
const filename = resolveRequestFilename(requestName);
if (!itemUid) {
const reqWithSameNameExists = find(collection.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename));
@ -725,44 +488,6 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
}
}
}
return;
}
if (!itemUid) {
collectionCopy.items.push(item);
} else {
const currentItem = findItemInCollection(collectionCopy, itemUid);
if (currentItem && currentItem.type === 'folder') {
currentItem.items = currentItem.items || [];
currentItem.items.push(item);
}
}
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_newItem({
item: item,
currentItemUid: itemUid,
collectionUid: collectionUid
})
);
})
.then(waitForNextTick)
.then(() => {
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item)
})
);
})
.then(() => resolve())
.catch(reject);
});
};
@ -774,30 +499,8 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
return reject(new Error('Collection not found'));
}
const environment = {
uid: uuid(),
name: name,
variables: []
};
const collectionCopy = cloneDeep(collection);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionToSave.environments = collectionToSave.environments || [];
collectionToSave.environments.push(environment);
if (isLocalCollection(collection)) {
environmentsSchema
.validate(collectionToSave.environments)
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, collectionToSave.environments))
.then(resolve)
.catch(reject);
return;
}
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_addEnvironment({ environment, collectionUid })))
ipcRenderer
.invoke('renderer:create-environment', collection.pathname, name)
.then(resolve)
.catch(reject);
});
@ -817,23 +520,12 @@ export const renameEnvironment = (newName, environmentUid, collectionUid) => (di
return reject(new Error('Environment not found'));
}
const oldName = environment.name;
environment.name = newName;
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
if (isLocalCollection(collection)) {
const environments = collectionToSave.environments;
environmentsSchema
.validate(environments)
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environments))
.then(resolve)
.catch(reject);
return;
}
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_renameEnvironment({ newName, environmentUid, collectionUid })))
environmentSchema
.validate(environment)
.then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, newName))
.then(resolve)
.catch(reject);
});
@ -854,23 +546,8 @@ export const deleteEnvironment = (environmentUid, collectionUid) => (dispatch, g
return reject(new Error('Environment not found'));
}
collectionCopy.environments = filter(collectionCopy.environments, (e) => e.uid !== environmentUid);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
if (isLocalCollection(collection)) {
const environments = collectionToSave.environments;
environmentsSchema
.validate(environments)
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environments))
.then(resolve)
.catch(reject);
return;
}
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_deleteEnvironment({ environmentUid, collectionUid })))
ipcRenderer
.invoke('renderer:delete-environment', collection.pathname, environment.name)
.then(resolve)
.catch(reject);
});
@ -892,21 +569,9 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di
environment.variables = variables;
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
if (isLocalCollection(collection)) {
const environments = collectionToSave.environments;
environmentsSchema
.validate(environments)
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environments))
.then(resolve)
.catch(reject);
return;
}
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_saveEnvironment({ variables, environmentUid, collectionUid })))
environmentSchema
.validate(environment)
.then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environment))
.then(resolve)
.catch(reject);
});
@ -928,19 +593,12 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g
}
}
collectionCopy.activeEnvironmentUid = environmentUid;
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => dispatch(_selectEnvironment({ environmentUid, collectionUid })))
.then(resolve)
.catch(reject);
dispatch(_selectEnvironment({ environmentUid, collectionUid }));
resolve();
});
};
export const removeLocalCollection = (collectionUid) => (dispatch, getState) => {
export const removeCollection = (collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@ -960,7 +618,7 @@ export const removeLocalCollection = (collectionUid) => (dispatch, getState) =>
.then(waitForNextTick)
.then(() => {
dispatch(
_deleteCollection({
_removeCollection({
collectionUid: collectionUid
})
);
@ -970,7 +628,7 @@ export const removeLocalCollection = (collectionUid) => (dispatch, getState) =>
});
};
export const browserLocalDirectory = () => (dispatch, getState) => {
export const browseDirectory = () => (dispatch, getState) => {
const { ipcRenderer } = window;
return new Promise((resolve, reject) => {
@ -978,8 +636,8 @@ export const browserLocalDirectory = () => (dispatch, getState) => {
});
};
export const openLocalCollectionEvent = (uid, pathname, name) => (dispatch, getState) => {
const localCollection = {
export const openCollectionEvent = (uid, pathname, name) => (dispatch, getState) => {
const collection = {
version: '1',
uid: uid,
name: name,
@ -989,14 +647,14 @@ export const openLocalCollectionEvent = (uid, pathname, name) => (dispatch, getS
return new Promise((resolve, reject) => {
collectionSchema
.validate(localCollection)
.then(() => dispatch(_createCollection(localCollection)))
.validate(collection)
.then(() => dispatch(_createCollection(collection)))
.then(resolve)
.catch(reject);
});
};
export const createLocalCollection = (collectionName, collectionLocation) => () => {
export const createCollection = (collectionName, collectionLocation) => () => {
const { ipcRenderer } = window;
return new Promise((resolve, reject) => {
@ -1004,7 +662,7 @@ export const createLocalCollection = (collectionName, collectionLocation) => ()
});
};
export const openLocalCollection = () => () => {
export const openCollection = () => () => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
@ -1012,8 +670,8 @@ export const openLocalCollection = () => () => {
});
};
export const localCollectionLoadEnvironmentsEvent = (payload) => (dispatch, getState) => {
const { data: environments, meta } = payload;
export const collectionAddEnvFileEvent = (payload) => (dispatch, getState) => {
const { data: environment, meta } = payload;
return new Promise((resolve, reject) => {
const state = getState();
@ -1022,12 +680,12 @@ export const localCollectionLoadEnvironmentsEvent = (payload) => (dispatch, getS
return reject(new Error('Collection not found'));
}
environmentsSchema
.validate(environments)
environmentSchema
.validate(environment)
.then(() =>
dispatch(
_localCollectionLoadEnvironmentsEvent({
environments,
_collectionAddEnvFileEvent({
environment,
collectionUid: meta.collectionUid
})
)
@ -1036,3 +694,14 @@ export const localCollectionLoadEnvironmentsEvent = (payload) => (dispatch, getS
.catch(reject);
});
};
export const importCollection = (collection, collectionLocation) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:import-collection', collection, collectionLocation)
.then(resolve)
.catch(reject);
});
};

View File

@ -32,26 +32,6 @@ export const collectionsSlice = createSlice({
name: 'collections',
initialState,
reducers: {
loadCollections: (state, action) => {
const collectionUids = map(state.collections, (c) => c.uid);
each(action.payload.collections, (c) => collapseCollection(c));
each(action.payload.collections, (c) => addDepth(c.items));
each(action.payload.collections, (c) => {
if (!collectionUids.includes(c.uid)) {
state.collections.push(c);
collectionUids.push(c.uid);
}
});
},
collectionImported: (state, action) => {
const collectionUids = map(state.collections, (c) => c.uid);
const { collection } = action.payload;
collapseCollection(collection);
addDepth(collection.items);
if (!collectionUids.includes(collection.uid)) {
state.collections.push(collection);
}
},
createCollection: (state, action) => {
const collectionUids = map(state.collections, (c) => c.uid);
const collection = action.payload;
@ -68,7 +48,7 @@ export const collectionsSlice = createSlice({
collection.name = action.payload.newName;
}
},
deleteCollection: (state, action) => {
removeCollection: (state, action) => {
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
},
addEnvironment: (state, action) => {
@ -80,28 +60,12 @@ export const collectionsSlice = createSlice({
collection.environments.push(environment);
}
},
renameEnvironment: (state, action) => {
const { newName, environmentUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
collectionUnlinkEnvFileEvent: (state, action) => {
const { data: environment, meta } = action.payload;
const collection = findCollectionByUid(state.collections, meta.collectionUid);
if (collection) {
const environment = findEnvironmentInCollection(collection, environmentUid);
if (environment) {
environment.name = newName;
}
}
},
deleteEnvironment: (state, action) => {
const { environmentUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
const environment = findEnvironmentInCollection(collection, environmentUid);
if (environment) {
collection.environments = filter(collection.environments, (e) => e.uid !== environmentUid);
}
collection.environments = filter(collection.environments, (e) => e.uid !== environment.uid);
}
},
saveEnvironment: (state, action) => {
@ -669,7 +633,7 @@ export const collectionsSlice = createSlice({
}
}
},
localCollectionAddFileEvent: (state, action) => {
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
@ -723,7 +687,7 @@ export const collectionsSlice = createSlice({
addDepth(collection.items);
}
},
localCollectionAddDirectoryEvent: (state, action) => {
collectionAddDirectoryEvent: (state, action) => {
const { dir } = action.payload;
const collection = findCollectionByUid(state.collections, dir.meta.collectionUid);
@ -751,7 +715,7 @@ export const collectionsSlice = createSlice({
addDepth(collection.items);
}
},
localCollectionChangeFileEvent: (state, action) => {
collectionChangeFileEvent: (state, action) => {
const { file } = action.payload;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
@ -768,7 +732,7 @@ export const collectionsSlice = createSlice({
}
}
},
localCollectionUnlinkFileEvent: (state, action) => {
collectionUnlinkFileEvent: (state, action) => {
const { file } = action.payload;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
@ -780,7 +744,7 @@ export const collectionsSlice = createSlice({
}
}
},
localCollectionUnlinkDirectoryEvent: (state, action) => {
collectionUnlinkDirectoryEvent: (state, action) => {
const { directory } = action.payload;
const collection = findCollectionByUid(state.collections, directory.meta.collectionUid);
@ -792,26 +756,31 @@ export const collectionsSlice = createSlice({
}
}
},
localCollectionLoadEnvironmentsEvent: (state, action) => {
const { environments, collectionUid } = action.payload;
collectionAddEnvFileEvent: (state, action) => {
const { environment, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.environments = environments;
collection.environments = collection.environments || [];
const existingEnv = collection.environments.find((e) => e.uid === environment.uid);
if (existingEnv) {
existingEnv.variables = environment.variables;
} else {
collection.environments.push(environment);
}
}
}
}
});
export const {
collectionImported,
createCollection,
renameCollection,
deleteCollection,
loadCollections,
removeCollection,
addEnvironment,
renameEnvironment,
deleteEnvironment,
collectionUnlinkEnvFileEvent,
saveEnvironment,
selectEnvironment,
newItem,
@ -844,12 +813,12 @@ export const {
updateRequestBody,
updateRequestGraphqlQuery,
updateRequestMethod,
localCollectionAddFileEvent,
localCollectionAddDirectoryEvent,
localCollectionChangeFileEvent,
localCollectionUnlinkFileEvent,
localCollectionUnlinkDirectoryEvent,
localCollectionLoadEnvironmentsEvent
collectionAddFileEvent,
collectionAddDirectoryEvent,
collectionChangeFileEvent,
collectionUnlinkFileEvent,
collectionUnlinkDirectoryEvent,
collectionAddEnvFileEvent
} = collectionsSlice.actions;
export default collectionsSlice.reducer;

View File

@ -1,222 +0,0 @@
import find from 'lodash/find';
import filter from 'lodash/filter';
import { uuid } from 'utils/common';
import cloneDeep from 'lodash/cloneDeep';
import { workspaceSchema } from '@usebruno/schema';
import { findCollectionInWorkspace } from 'utils/workspaces';
import { getWorkspacesFromIdb, saveWorkspaceToIdb, deleteWorkspaceInIdb } from 'utils/idb/workspaces';
import { BrunoError } from 'utils/common/error';
import {
loadWorkspaces,
addWorkspace as _addWorkspace,
renameWorkspace as _renameWorkspace,
deleteWorkspace as _deleteWorkspace,
addCollectionToWorkspace as _addCollectionToWorkspace,
removeCollectionFromWorkspace as _removeCollectionFromWorkspace
} from './index';
const seedWorkpace = () => {
const uid = uuid();
const workspace = {
uid: uid,
name: 'My workspace',
collections: []
};
return new Promise((resolve, reject) => {
workspaceSchema
.validate(workspace)
.then(() => saveWorkspaceToIdb(window.__idb, workspace))
.then(() => resolve([workspace]))
.catch(reject);
});
};
export const loadWorkspacesFromIdb = () => (dispatch) => {
return new Promise((resolve, reject) => {
getWorkspacesFromIdb(window.__idb)
.then((workspaces) => {
if (!workspaces || !workspaces.length) {
return seedWorkpace();
}
return workspaces;
})
.then((workspaces) =>
dispatch(
loadWorkspaces({
workspaces: workspaces
})
)
)
.then(resolve)
.catch(reject);
});
};
export const addWorkspace = (workspaceName) => (dispatch) => {
const newWorkspace = {
uid: uuid(),
name: workspaceName,
collections: []
};
return new Promise((resolve, reject) => {
workspaceSchema
.validate(newWorkspace)
.then(() => saveWorkspaceToIdb(window.__idb, newWorkspace))
.then(() =>
dispatch(
_addWorkspace({
workspace: newWorkspace
})
)
)
.then(resolve)
.catch(reject);
});
};
export const renameWorkspace = (newName, uid) => (dispatch, getState) => {
const state = getState();
return new Promise((resolve, reject) => {
const workspace = find(state.workspaces.workspaces, (w) => w.uid === uid);
if (!workspace) {
return reject(new Error('Workspace not found'));
}
const workspaceCopy = cloneDeep(workspace);
workspaceCopy.name = newName;
workspaceSchema
.validate(workspaceCopy)
.then(() => saveWorkspaceToIdb(window.__idb, workspaceCopy))
.then(() =>
dispatch(
_renameWorkspace({
uid: uid,
name: newName
})
)
)
.then(resolve)
.catch(reject);
});
};
export const deleteWorkspace = (workspaceUid) => (dispatch, getState) => {
const state = getState();
return new Promise((resolve, reject) => {
if (state.workspaces.activeWorkspaceUid === workspaceUid) {
throw new BrunoError('Cannot delete current workspace');
}
const workspace = find(state.workspaces.workspaces, (w) => w.uid === workspaceUid);
if (!workspace) {
return reject(new Error('Workspace not found'));
}
deleteWorkspaceInIdb(window.__idb, workspaceUid)
.then(() =>
dispatch(
_deleteWorkspace({
workspaceUid: workspaceUid
})
)
)
.then(resolve)
.catch(reject);
});
};
export const addCollectionToWorkspace = (workspaceUid, collectionUid) => (dispatch, getState) => {
const state = getState();
return new Promise((resolve, reject) => {
const workspace = find(state.workspaces.workspaces, (w) => w.uid === workspaceUid);
const collection = find(state.collections.collections, (c) => c.uid === collectionUid);
if (!workspace) {
return reject(new Error('Workspace not found'));
}
if (!collection) {
return reject(new Error('Collection not found'));
}
const workspaceCopy = cloneDeep(workspace);
if (workspaceCopy.collections && workspace.collections.length) {
if (!findCollectionInWorkspace(workspace, collectionUid)) {
workspaceCopy.collections.push({
uid: collectionUid
});
}
} else {
workspaceCopy.collections = [
{
uid: collectionUid
}
];
}
workspaceSchema
.validate(workspaceCopy)
.then(() => saveWorkspaceToIdb(window.__idb, workspaceCopy))
.then(() =>
dispatch(
_addCollectionToWorkspace({
workspaceUid: workspaceUid,
collectionUid: collectionUid
})
)
)
.then(resolve)
.catch(reject);
});
};
export const removeCollectionFromWorkspace = (workspaceUid, collectionUid) => (dispatch, getState) => {
const state = getState();
return new Promise((resolve, reject) => {
const workspace = find(state.workspaces.workspaces, (w) => w.uid === workspaceUid);
const collection = find(state.collections.collections, (c) => c.uid === collectionUid);
if (!workspace) {
return reject(new Error('Workspace not found'));
}
if (!collection) {
return reject(new Error('Collection not found'));
}
const workspaceCopy = cloneDeep(workspace);
if (workspaceCopy.collections && workspace.collections.length) {
workspaceCopy.collections = filter(workspaceCopy.collections, (c) => c.uid !== collectionUid);
}
workspaceSchema
.validate(workspaceCopy)
.then(() => saveWorkspaceToIdb(window.__idb, workspaceCopy))
.then(() =>
dispatch(
_removeCollectionFromWorkspace({
workspaceUid: workspaceUid,
collectionUid: collectionUid
})
)
)
.then(resolve)
.catch(reject);
});
};
// TODO
// Workspaces can have collection uids that no longer exist
// or the user may have the collections access revoked (in teams)
// This action will have to be called at the beginning to purge any zombi collection references in the workspaces
export const removeZombieCollectionFromAllWorkspaces = (workspaceUid) => (dispatch, getState) => {};

View File

@ -1,85 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
import find from 'lodash/find';
import map from 'lodash/map';
import filter from 'lodash/filter';
import { findCollectionInWorkspace } from 'utils/workspaces';
import cache from 'utils/common/cache';
const initialState = {
workspaces: [],
activeWorkspaceUid: null
};
export const workspacesSlice = createSlice({
name: 'workspaces',
initialState,
reducers: {
loadWorkspaces: (state, action) => {
state.workspaces = action.payload.workspaces;
if (state.workspaces && state.workspaces.length) {
const workspaceUids = map(state.workspaces, (w) => w.uid);
const activeWorkspaceUid = cache.getActiveWorkspaceUid();
if (activeWorkspaceUid && workspaceUids.includes(activeWorkspaceUid)) {
state.activeWorkspaceUid = activeWorkspaceUid;
} else {
state.activeWorkspaceUid = state.workspaces[0].uid;
cache.setActiveWorkspaceUid(state.activeWorkspaceUid);
}
}
},
selectWorkspace: (state, action) => {
state.activeWorkspaceUid = action.payload.workspaceUid;
cache.setActiveWorkspaceUid(state.activeWorkspaceUid);
},
renameWorkspace: (state, action) => {
const { name, uid } = action.payload;
const workspace = find(state.workspaces, (w) => w.uid === uid);
if (workspace) {
workspace.name = name;
}
},
deleteWorkspace: (state, action) => {
if (state.activeWorkspaceUid === action.payload.workspaceUid) {
throw new Error('User cannot delete current workspace');
}
state.workspaces = state.workspaces.filter((workspace) => workspace.uid !== action.payload.workspaceUid);
},
addWorkspace: (state, action) => {
state.workspaces.push(action.payload.workspace);
},
addCollectionToWorkspace: (state, action) => {
const { workspaceUid, collectionUid } = action.payload;
const workspace = find(state.workspaces, (w) => w.uid === workspaceUid);
if (workspace) {
if (workspace.collections && workspace.collections.length) {
if (!findCollectionInWorkspace(workspace, collectionUid)) {
workspace.collections.push({
uid: collectionUid
});
}
} else {
workspace.collections = [
{
uid: collectionUid
}
];
}
}
},
removeCollectionFromWorkspace: (state, action) => {
const { workspaceUid, collectionUid } = action.payload;
const workspace = find(state.workspaces, (w) => w.uid === workspaceUid);
if (workspace && workspace.collections && workspace.collections.length) {
workspace.collections = filter(workspace.collections, (c) => c.uid !== collectionUid);
}
}
}
});
export const { loadWorkspaces, selectWorkspace, renameWorkspace, deleteWorkspace, addWorkspace, addCollectionToWorkspace, removeCollectionFromWorkspace } = workspacesSlice.actions;
export default workspacesSlice.reducer;

View File

@ -24,7 +24,7 @@ const darkTheme = {
bg: '#252526',
dragbar: '#8a8a8a',
workspace: {
badge: {
bg: '#3D3D3D'
},

View File

@ -24,7 +24,7 @@ const lightTheme = {
bg: '#F3F3F3',
dragbar: 'rgb(200, 200, 200)',
workspace: {
badge: {
bg: '#e1e1e1'
},

View File

@ -1,6 +1,37 @@
import * as FileSaver from 'file-saver';
import get from 'lodash/get';
import each from 'lodash/each';
const deleteUidsInItems = (items) => {
each(items, (item) => {
delete item.uid;
if (['http-request', 'graphql-request'].includes(item.type)) {
each(get(item, 'request.headers'), (header) => delete header.uid);
each(get(item, 'request.params'), (param) => delete param.uid);
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
}
if (item.items && item.items.length) {
deleteUidsInItems(item.items);
}
});
};
const deleteUidsInEnvs = (envs) => {
each(envs, (env) => {
delete env.uid;
each(env.variables, (variable) => delete variable.uid);
});
};
const exportCollection = (collection) => {
// delete uids
delete collection.uid;
deleteUidsInItems(collection.items);
deleteUidsInEnvs(collection.environments);
const fileName = `${collection.name}.json`;
const fileBlob = new Blob([JSON.stringify(collection, null, 2)], { type: 'application/json' });

View File

@ -5,14 +5,6 @@ class Cache {
set(key, val) {
window.localStorage.setItem(key, val);
}
getActiveWorkspaceUid() {
return this.get('bruno.activeWorkspaceUid');
}
setActiveWorkspaceUid(workspaceUid) {
this.set('bruno.activeWorkspaceUid', workspaceUid);
}
}
module.exports = new Cache();

View File

@ -13,20 +13,6 @@ export const saveCollectionToIdb = (connection, collection) => {
});
};
export const deleteCollectionInIdb = (connection, collectionUid) => {
return new Promise((resolve, reject) => {
connection
.then((db) => {
let tx = db.transaction(`collection`, 'readwrite');
tx.objectStore('collection').delete(collectionUid);
tx.oncomplete = () => resolve(collectionUid);
tx.onerror = () => reject(tx.error);
})
.catch((err) => reject(err));
});
};
export const getCollectionsFromIdb = (connection) => {
return new Promise((resolve, reject) => {
connection

View File

@ -1,59 +0,0 @@
import isArray from 'lodash/isArray';
export const saveWorkspaceToIdb = (connection, workspace) => {
return new Promise((resolve, reject) => {
connection
.then((db) => {
let tx = db.transaction(`workspace`, 'readwrite');
let workspaceStore = tx.objectStore('workspace');
if (isArray(workspace)) {
for (let c of workspace) {
workspaceStore.put(c);
}
} else {
workspaceStore.put(workspace);
}
return new Promise((res, rej) => {
tx.oncomplete = () => res();
tx.onerror = () => rej(tx.error);
});
})
.then(resolve)
.catch((err) => reject(err));
});
};
export const deleteWorkspaceInIdb = (connection, workspaceUid) => {
return new Promise((resolve, reject) => {
connection
.then((db) => {
let tx = db.transaction(`workspace`, 'readwrite');
tx.objectStore('workspace').delete(workspaceUid);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
})
.catch((err) => reject(err));
});
};
export const getWorkspacesFromIdb = (connection) => {
return new Promise((resolve, reject) => {
connection
.then((db) => {
let tx = db.transaction('workspace');
let workspaceStore = tx.objectStore('workspace');
return workspaceStore.getAll();
})
.then((workspaces) => {
if (!Array.isArray(workspaces)) {
return new Error('IDB Corrupted');
}
return resolve(workspaces);
})
.catch((err) => reject(err));
});
};

View File

@ -1,8 +1,6 @@
import fileDialog from 'file-dialog';
import { saveCollectionToIdb } from 'utils/idb';
import { BrunoError } from 'utils/common/error';
import { validateSchema, updateUidsInCollection } from './common';
import sampleCollection from './samples/sample-collection.json';
const readFile = (files) => {
return new Promise((resolve, reject) => {
@ -30,10 +28,8 @@ const importCollection = () => {
fileDialog({ accept: 'application/json' })
.then(readFile)
.then(parseJsonCollection)
.then(validateSchema)
.then(updateUidsInCollection)
.then(validateSchema)
.then((collection) => saveCollectionToIdb(window.__idb, collection))
.then((collection) => resolve(collection))
.catch((err) => {
console.log(err);
@ -42,15 +38,4 @@ const importCollection = () => {
});
};
export const importSampleCollection = () => {
return new Promise((resolve, reject) => {
validateSchema(sampleCollection)
.then(updateUidsInCollection)
.then(validateSchema)
.then((collection) => saveCollectionToIdb(window.__idb, collection))
.then(resolve)
.catch(reject);
});
};
export default importCollection;

View File

@ -40,5 +40,14 @@ export const updateUidsInCollection = (_collection) => {
};
updateItemUids(collection.items);
const updateEnvUids = (envs = []) => {
each(envs, (env) => {
env.uid = uuid();
each(env.variables, (variable) => (variable.uid = uuid()));
});
};
updateEnvUids(collection.environments);
updateEnvUids(collection.environments);
return collection;
};

View File

@ -2,7 +2,6 @@ import each from 'lodash/each';
import get from 'lodash/get';
import fileDialog from 'file-dialog';
import { uuid } from 'utils/common';
import { saveCollectionToIdb } from 'utils/idb';
import { BrunoError } from 'utils/common/error';
import { validateSchema, updateUidsInCollection } from './common';
@ -180,9 +179,6 @@ const importCollection = () => {
.then(readFile)
.then(parsePostmanCollection)
.then(validateSchema)
.then(updateUidsInCollection)
.then(validateSchema)
.then((collection) => saveCollectionToIdb(window.__idb, collection))
.then((collection) => resolve(collection))
.catch((err) => {
console.log(err);

View File

@ -1,129 +0,0 @@
{
"name": "sample-collection",
"uid": "c1PdISj460OeNmFLI8rCY",
"version": "1",
"items": [
{
"uid": "myeFbgzNlwIhDpYcOVBWS",
"type": "http-request",
"name": "Users",
"request": {
"url": "https://reqres.in/api/users?page=2",
"method": "GET",
"headers": [],
"params": [
{
"uid": "viHGWSpAQhpiwH9UQxb7W",
"name": "page",
"value": "2",
"enabled": true
}
],
"body": {
"mode": "json",
"json": "",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
},
{
"uid": "ytzXIADbhEwLB0BqIQOKD",
"type": "http-request",
"name": "Single User",
"request": {
"url": "https://reqres.in/api/users/2",
"method": "GET",
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
},
{
"uid": "DuEyHudhVuxrdplKLAMsU",
"type": "http-request",
"name": "User Not Found",
"request": {
"url": "https://reqres.in/api/users/23",
"method": "GET",
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
},
{
"uid": "UZFjsr14q8iZ1Do90q02Q",
"type": "http-request",
"name": "Create",
"request": {
"url": "https://reqres.in/api/users",
"method": "POST",
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"name\": \"morpheus\",\n \"job\": \"leader\"\n}",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
},
{
"uid": "lYLCTXaerD9etRDLJHbVN",
"type": "http-request",
"name": "Update",
"request": {
"url": "https://reqres.in/api/users/2",
"method": "PUT",
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"name\": \"morpheus\",\n \"job\": \"zion resident\"\n}",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
},
{
"uid": "qVCKx5xUqxpBf8jeka8Gu",
"type": "http-request",
"name": "Remove",
"request": {
"url": "https://reqres.in/api/users/2",
"method": "DELETE",
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"name\": \"morpheus\",\n \"job\": \"zion resident\"\n}",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
}
],
"environments": []
}

View File

@ -1,5 +0,0 @@
import find from 'lodash/find';
export const findCollectionInWorkspace = (workspace, collectionUid) => {
return find(workspace.collections, (c) => c.uid === collectionUid);
};

View File

@ -5,13 +5,14 @@ const Yup = require('yup');
const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
// uid inside collections is deprecated, but we still need to validate it
// for backward compatibility
const uidSchema = Yup.string()
.length(21, 'uid must be 21 characters in length')
.matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric')
.required('uid is required')
.strict();
.matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric');
const configSchema = Yup.object({
uid: uidSchema,
name: Yup.string().nullable().max(256, 'name must be 256 characters or less'),
type: Yup.string().oneOf(['collection']).required('type is required'),
version: Yup.string().oneOf(['1']).required('type is required')

View File

@ -5,17 +5,27 @@ const chokidar = require('chokidar');
const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem');
const {
bruToJson,
jsonToBru
jsonToBru,
bruToEnvJson,
envJsonToBru,
} = require('@usebruno/bruno-lang');
const { itemSchema } = require('@usebruno/schema');
const { generateUidBasedOnHash, uuid } = require('../utils/common');
const isEnvironmentConfig = (pathname, collectionPath) => {
const isJsonEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
return dirname === collectionPath && basename === 'environments.json';
}
};
const isBruEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const envDirectory = path.join(collectionPath, 'environments');
const basename = path.basename(pathname);
return dirname === envDirectory && hasBruExtension(basename);
};
const hydrateRequestWithUuid = (request, pathname) => {
request.uid = generateUidBasedOnHash(pathname);
@ -35,16 +45,21 @@ const hydrateRequestWithUuid = (request, pathname) => {
const addEnvironmentFile = async (win, pathname, collectionUid) => {
try {
const basename = path.basename(pathname);
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname),
}
name: basename
},
};
const jsonData = fs.readFileSync(pathname, 'utf8');
file.data = JSON.parse(jsonData);
const bruContent = fs.readFileSync(pathname, 'utf8');
file.data = bruToEnvJson(bruContent);
file.data.name = basename.substring(0, basename.length - 4);
file.data.uid = generateUidBasedOnHash(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
} catch (err) {
console.error(err)
@ -53,17 +68,25 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => {
const changeEnvironmentFile = async (win, pathname, collectionUid) => {
try {
const basename = path.basename(pathname);
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname),
name: basename
}
};
const jsonData = fs.readFileSync(pathname, 'utf8');
file.data = JSON.parse(jsonData);
win.webContents.send('main:collection-tree-updated', 'changeEnvironmentFile', file);
const bruContent = fs.readFileSync(pathname, 'utf8');
file.data = bruToEnvJson(bruContent);
file.data.name = basename.substring(0, basename.length - 4);
file.data.uid = generateUidBasedOnHash(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => variable.uid = uuid());
// we are reusing the addEnvironmentFile event itself
// this is because the uid of the pathname remains the same
// and the collection tree will be able to update the existing environment
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
} catch (err) {
console.error(err)
}
@ -77,23 +100,49 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
pathname,
name: path.basename(pathname),
},
data: []
data: {
uid: generateUidBasedOnHash(pathname),
name: path.basename(pathname).substring(0, path.basename(pathname).length - 4),
}
};
win.webContents.send('main:collection-tree-updated', 'changeEnvironmentFile', file);
win.webContents.send('main:collection-tree-updated', 'unlinkEnvironmentFile', file);
} catch (err) {
console.error(err)
}
};
const add = async (win, pathname, collectionUid, collectionPath) => {
const isJson = hasJsonExtension(pathname);
console.log(`watcher add: ${pathname}`);
if(isJson) {
if(isEnvironmentConfig(pathname, collectionPath)) {
return addEnvironmentFile(win, pathname, collectionUid);
if(isJsonEnvironmentConfig(pathname, collectionPath)) {
// migrate old env json to bru file
try {
const dirname = path.dirname(pathname);
const jsonStr = fs.readFileSync(pathname, 'utf8');
const jsonData = JSON.parse(jsonStr);
const envDirectory = path.join(dirname, 'environments');
if (!fs.existsSync(envDirectory)) {
fs.mkdirSync(envDirectory);
}
for(const env of jsonData) {
const bruEnvFilename = path.join(envDirectory, `${env.name}.bru`);
const bruContent = envJsonToBru(env);
await writeFile(bruEnvFilename, bruContent);
}
await fs.unlinkSync(pathname);
} catch (err) {
// do nothing
}
return;
}
if(isBruEnvironmentConfig(pathname, collectionPath)) {
return addEnvironmentFile(win, pathname, collectionUid);
}
// migrate old json files to bru
@ -137,8 +186,14 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
}
};
const addDirectory = (win, pathname, collectionUid) => {
console.log(`watcher addDirectory: ${pathname}`);
const addDirectory = (win, pathname, collectionUid, collectionPath) => {
const dirname = path.dirname(pathname);
const envDirectory = path.join(collectionPath, 'environments');
if(dirname === envDirectory) {
return;
}
const directory = {
meta: {
collectionUid,
@ -150,9 +205,7 @@ const addDirectory = (win, pathname, collectionUid) => {
};
const change = async (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher change: ${pathname}`);
if(isEnvironmentConfig(pathname, collectionPath)) {
if(isBruEnvironmentConfig(pathname, collectionPath)) {
return changeEnvironmentFile(win, pathname, collectionUid);
}
@ -178,7 +231,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
};
const unlink = (win, pathname, collectionUid, collectionPath) => {
if(isEnvironmentConfig(pathname, collectionPath)) {
if(isBruEnvironmentConfig(pathname, collectionPath)) {
return unlinkEnvironmentFile(win, pathname, collectionUid);
}
@ -195,6 +248,13 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
}
const unlinkDir = (win, pathname, collectionUid) => {
const dirname = path.dirname(pathname);
const envDirectory = path.join(collectionPath, 'environments');
if(dirname === envDirectory) {
return;
}
const directory = {
meta: {
collectionUid,

View File

@ -5,6 +5,7 @@ const { ipcMain } = require('electron');
const {
jsonToBru,
bruToJson,
envJsonToBru,
} = require('@usebruno/bruno-lang');
const {
isValidPathname,
@ -16,6 +17,7 @@ const {
} = require('../utils/filesystem');
const { uuid, stringifyJson } = require('../utils/common');
const { openCollectionDialog, openCollection } = require('../app/collections');
const { generateUidBasedOnHash } = require('../utils/common');
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
// browse directory
@ -43,10 +45,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(dirPath);
const uid = uuid();
const uid = generateUidBasedOnHash(dirPath);
const content = await stringifyJson({
version: '1',
uid: uid,
name: collectionName,
type: 'collection'
});
@ -89,18 +90,83 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
// save environment
ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environments) => {
// create environment
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name) => {
try {
const envFilePath = path.join(collectionPathname, 'environments.json');
const envDirPath = path.join(collectionPathname, 'environments');
if (!fs.existsSync(envDirPath)){
await createDirectory(envDirPath);
}
const content = await stringifyJson(environments);
const envFilePath = path.join(envDirPath, `${name}.bru`);
if (fs.existsSync(envFilePath)){
throw new Error(`environment: ${envFilePath} already exists`);
}
const content = envJsonToBru({
variables: []
});
await writeFile(envFilePath, content);
} catch (error) {
return Promise.reject(error);
}
});
// save environment
ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
if (!fs.existsSync(envDirPath)){
await createDirectory(envDirPath);
}
const envFilePath = path.join(envDirPath, `${environment.name}.bru`);
if (!fs.existsSync(envFilePath)){
throw new Error(`environment: ${envFilePath} does not exist`);
}
const content = envJsonToBru(environment);
await writeFile(envFilePath, content);
} catch (error) {
return Promise.reject(error);
}
});
// rename environment
ipcMain.handle('renderer:rename-environment', async (event, collectionPathname, environmentName, newName) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
if (!fs.existsSync(envFilePath)){
throw new Error(`environment: ${envFilePath} does not exist`);
}
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
if (fs.existsSync(newEnvFilePath)){
throw new Error(`environment: ${newEnvFilePath} already exists`);
}
fs.renameSync(envFilePath, newEnvFilePath);
} catch (error) {
return Promise.reject(error);
}
});
// delete environment
ipcMain.handle('renderer:delete-environment', async (event, collectionPathname, environmentName) => {
try {
const envDirPath = path.join(collectionPathname, 'environments');
const envFilePath = path.join(envDirPath, `${environmentName}.bru`);
if (!fs.existsSync(envFilePath)){
throw new Error(`environment: ${envFilePath} does not exist`);
}
fs.unlinkSync(envFilePath);
} catch (error) {
return Promise.reject(error);
}
});
// rename item
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
@ -177,6 +243,71 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
try {
let collectionName = collection.name;
let collectionPath = path.join(collectionLocation, collectionName);
if (fs.existsSync(collectionPath)){
throw new Error(`collection: ${collectionPath} already exists`);
}
// Recursive function to parse the collection items and create files/folders
const parseCollectionItems = (items = [], currentPath) => {
items.forEach(item => {
if (item.type === 'http-request') {
const content = jsonToBru(item);
const filePath = path.join(currentPath, `${item.name}.bru`);
fs.writeFileSync(filePath, content);
}
if (item.type === 'folder') {
const folderPath = path.join(currentPath, item.name);
fs.mkdirSync(folderPath);
if(item.items && item.items.length) {
parseCollectionItems(item.items, folderPath);
}
}
});
};
const parseEnvironments = (environments = [], collectionPath) => {
const envDirPath = path.join(collectionPath, 'environments');
if(!fs.existsSync(envDirPath)){
fs.mkdirSync(envDirPath);
}
environments.forEach(env => {
const content = envJsonToBru(env);
const filePath = path.join(envDirPath, `${env.name}.bru`);
fs.writeFileSync(filePath, content);
});
};
await createDirectory(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
const content = await stringifyJson({
version: '1',
name: collection.name,
type: 'collection'
});
await writeFile(path.join(collectionPath, 'bruno.json'), content);
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, collectionName);
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid);
lastOpenedCollections.add(collectionPath);
// create folder and files based on collection
await parseCollectionItems(collection.items, collectionPath);
await parseEnvironments(collection.environments, collectionPath);
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:ready', async (event) => {
// reload last opened collections
const lastOpened = lastOpenedCollections.getAll();

View File

@ -63,15 +63,16 @@ const bodyXmlTag = between(bodyXmlBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((
// generic key value parser
const newline = regex(/^\r?\n/);
const newLineOrEndOfInput = choice([newline, endOfInput]);
const word = regex(/^[^\s\t\n]+/g);
const wordWithoutWhitespace = regex(/^[^\s\t\n]+/g);
const wordWithWhitespace = regex(/^[^\n]+/g);
const line = sequenceOf([
optionalWhitespace,
digit,
whitespace,
word,
wordWithoutWhitespace,
whitespace,
word,
wordWithWhitespace,
newLineOrEndOfInput
]).map(([_, enabled, __, key, ___, value]) => {
return {

View File

@ -0,0 +1,47 @@
const {
sequenceOf,
whitespace,
optionalWhitespace,
choice,
endOfInput,
between,
digit,
many,
regex,
sepBy
} = require("arcsecond");
const newline = regex(/^\r?\n/);
const newLineOrEndOfInput = choice([newline, endOfInput]);
const begin = regex(/^vars\s*\r?\n/);
const end = regex(/^[\r?\n]*\/vars\s*[\r?\n]*/);
const wordWithoutWhitespace = regex(/^[^\s\t\n]+/g);
const wordWithWhitespace = regex(/^[^\n]+/g);
const line = sequenceOf([
optionalWhitespace,
digit,
whitespace,
wordWithoutWhitespace,
whitespace,
wordWithWhitespace,
newLineOrEndOfInput
]).map(([_, enabled, __, key, ___, value]) => {
return {
"enabled": Number(enabled) ? true : false,
"name": key,
"value": value,
"type": "text"
};
});
const lines = many(line);
const envVarsLines = sepBy(newline)(lines);
const envVarsTag = between(begin)(end)(envVarsLines).map(([variables]) => {
return {
variables
};
});
module.exports = envVarsTag;

View File

@ -16,15 +16,16 @@ const newLineOrEndOfInput = choice([newline, endOfInput]);
const begin = regex(/^headers\s*\r?\n/);
const end = regex(/^[\r?\n]*\/headers\s*[\r?\n]*/);
const word = regex(/^[^\s\t\n]+/g);
const wordWithoutWhitespace = regex(/^[^\s\t\n]+/g);
const wordWithWhitespace = regex(/^[^\n]+/g);
const line = sequenceOf([
optionalWhitespace,
digit,
whitespace,
word,
wordWithoutWhitespace,
whitespace,
word,
wordWithWhitespace,
newLineOrEndOfInput
]).map(([_, enabled, __, key, ___, value]) => {
return {

View File

@ -51,7 +51,7 @@ const bruToJson = (fileContents) => {
headers: parsed.headers || [],
body: parsed.body || {mode: 'none'}
}
}
};
const body = get(json, 'request.body');
@ -161,7 +161,47 @@ ${body.multipartForm.map(item => ` ${item.enabled ? 1 : 0} ${item.name} ${item.
return bru;
};
// env
const envVarsTag = require('./env-vars-tag');
const bruToEnvJson = (fileContents) => {
const parser = many(choice([
envVarsTag,
anyChar
]));
const parsed = parser
.run(fileContents)
.result
.reduce((acc, item) => _.merge(acc, item), {});
const json = {
variables: parsed.variables || []
};
return json;
};
const envJsonToBru = (json) => {
const {
variables
} = json;
let bru = '';
if(variables && variables.length) {
bru += `vars
${variables.map(item => ` ${item.enabled ? 1 : 0} ${item.name} ${item.value}`).join('\n')}
/vars
`;
}
return bru;
};
module.exports = {
bruToJson,
jsonToBru
jsonToBru,
bruToEnvJson,
envJsonToBru
};

View File

@ -17,15 +17,16 @@ const newLineOrEndOfInput = choice([newline, endOfInput]);
const begin = regex(/^params\s*\r?\n/);
const end = regex(/^[\r?\n]*\/params\s*[\r?\n]*/);
const word = regex(/^[^\s\t\n]+/g);
const wordWithoutWhitespace = regex(/^[^\s\t\n]+/g);
const wordWithWhitespace = regex(/^[^\n]+/g);
const line = sequenceOf([
optionalWhitespace,
digit,
whitespace,
word,
wordWithoutWhitespace,
whitespace,
word,
wordWithWhitespace,
newLineOrEndOfInput
]).map(([_, enabled, __, key, ___, value]) => {
return {

View File

@ -0,0 +1,33 @@
const fs = require('fs');
const path = require('path');
const {
bruToEnvJson
} = require('../src');
describe('bruToEnvJson', () => {
it('should parse .bru file contents', () => {
const requestFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'env.bru'), 'utf8');
const result = bruToEnvJson(requestFile);
expect(result).toEqual({
"variables": [{
"enabled": true,
"name": "host",
"value": "https://www.google.com",
"type": "text"
}, {
"enabled": true,
"name": "jwt",
"value": "secret",
"type": "text"
}, {
"enabled": false,
"name": "Content-type",
"value": "application/json",
"type": "text"
}]
});
});
});

View File

@ -0,0 +1,35 @@
const fs = require('fs');
const path = require('path');
const {
envJsonToBru
} = require('../src');
describe('envJsonToBru', () => {
it('should convert json file into .bru file', () => {
const env = {
"variables": [{
"enabled": true,
"name": "host",
"value": "https://www.google.com",
"type": "text"
}, {
"enabled": true,
"name": "jwt",
"value": "secret",
"type": "text"
}, {
"enabled": false,
"name": "Content-type",
"value": "application/json",
"type": "text"
}]
};
const expectedBruFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'env.bru'), 'utf8');
const actualBruFile = envJsonToBru(env);
expect(expectedBruFile).toEqual(actualBruFile);
});
});

View File

@ -0,0 +1,5 @@
vars
1 host https://www.google.com
1 jwt secret
0 Content-type application/json
/vars

View File

@ -12,7 +12,7 @@ const environmentVariablesSchema = Yup.object({
const environmentSchema = Yup.object({
uid: uidSchema,
name: Yup.string().min(1).max(50, 'name must be 50 characters or less').required('name is required'),
name: Yup.string().min(1).max(50, 'name must be 100 characters or less').required('name is required'),
variables: Yup.array().of(environmentVariablesSchema).required('variables are required')
}).noUnknown(true).strict();
@ -91,6 +91,7 @@ const collectionSchema = Yup.object({
module.exports = {
requestSchema,
itemSchema,
environmentSchema,
environmentsSchema,
collectionSchema
};

View File

@ -1,8 +1,9 @@
const { workspaceSchema } = require("./workspaces");
const { collectionSchema, itemSchema, environmentsSchema } = require("./collections");
const { collectionSchema, itemSchema, environmentSchema, environmentsSchema } = require("./collections");
module.exports = {
itemSchema,
environmentSchema,
environmentsSchema,
collectionSchema,
workspaceSchema