feat: clone request, resolves #3

This commit is contained in:
Anoop M D 2022-09-29 02:02:17 +05:30
parent c7cced5868
commit 85677b8a6c
6 changed files with 162 additions and 19 deletions

View File

@ -0,0 +1,66 @@
import React, { useRef, useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { cloneItem } from 'providers/ReduxStore/slices/collections';
const CloneCollectionItem = ({collection, item, onClose}) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: item.name
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
dispatch(cloneItem(values.name, item.uid, collection.uid));
onClose();
}
});
useEffect(() => {
if(inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onSubmit = () => formik.handleSubmit();
return (
<Modal
size="sm"
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
confirmText='Clone'
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">{isFolder ? 'Folder' : 'Request'} Name</label>
<input
id="collection-item-name" type="text" name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}
</div>
</form>
</Modal>
);
};
export default CloneCollectionItem;

View File

@ -10,6 +10,7 @@ import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder'; import NewFolder from 'components/Sidebar/NewFolder';
import RequestMethod from './RequestMethod'; import RequestMethod from './RequestMethod';
import RenameCollectionItem from './RenameCollectionItem'; import RenameCollectionItem from './RenameCollectionItem';
import CloneCollectionItem from './CloneCollectionItem';
import DeleteCollectionItem from './DeleteCollectionItem'; import DeleteCollectionItem from './DeleteCollectionItem';
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs'; import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
@ -22,6 +23,7 @@ const CollectionItem = ({item, collection}) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false); const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false); const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false); const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
@ -74,6 +76,7 @@ const CollectionItem = ({item, collection}) => {
return ( return (
<StyledWrapper className={className}> <StyledWrapper className={className}>
{renameItemModalOpen && <RenameCollectionItem item={item} collection={collection} onClose={() => setRenameItemModalOpen(false)}/>} {renameItemModalOpen && <RenameCollectionItem item={item} collection={collection} onClose={() => setRenameItemModalOpen(false)}/>}
{cloneItemModalOpen && <CloneCollectionItem item={item} collection={collection} onClose={() => setCloneItemModalOpen(false)}/>}
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)}/>} {deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)}/>}
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)}/>} {newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)}/>}
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)}/>} {newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)}/>}
@ -137,6 +140,14 @@ const CollectionItem = ({item, collection}) => {
}}> }}>
Rename Rename
</div> </div>
{!isFolder && (
<div className="dropdown-item" onClick={(e) => {
dropdownTippyRef.current.hide();
setCloneItemModalOpen(true);
}}>
Clone
</div>
)}
<div className="dropdown-item delete-item" onClick={(e) => { <div className="dropdown-item delete-item" onClick={(e) => {
dropdownTippyRef.current.hide(); dropdownTippyRef.current.hide();
setDeleteItemModalOpen(true); setDeleteItemModalOpen(true);

View File

@ -4,6 +4,7 @@ import Collection from './Collection';
const Collections = () => { const Collections = () => {
const collections = useSelector((state) => state.collections.collections); const collections = useSelector((state) => state.collections.collections);
console.log(collections);
return ( return (
<div className="mt-4 flex flex-col"> <div className="mt-4 flex flex-col">

View File

@ -6,7 +6,7 @@ const GlobalStyle = createGlobalStyle`
border-right: solid 1px var(--color-codemirror-border); border-right: solid 1px var(--color-codemirror-border);
} }
.grafnode-form { .bruno-form {
.textbox { .textbox {
line-height: 1.42857143; line-height: 1.42857143;
background-color: #fff; background-color: #fff;

View File

@ -1,9 +1,9 @@
import path from 'path'; import path from 'path';
import { uuid } from 'utils/common'; import { uuid } from 'utils/common';
import trim from 'lodash/trim';
import find from 'lodash/find'; import find from 'lodash/find';
import concat from 'lodash/concat'; import concat from 'lodash/concat';
import filter from 'lodash/filter'; import filter from 'lodash/filter';
import each from 'lodash/each';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { createSlice } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit'
import splitOnFirst from 'split-on-first'; import splitOnFirst from 'split-on-first';
@ -11,15 +11,15 @@ import { sendNetworkRequest } from 'utils/network';
import { import {
findCollectionByUid, findCollectionByUid,
findItemInCollection, findItemInCollection,
cloneItem, findParentItemInCollection,
transformCollectionToSaveToIdb, transformCollectionToSaveToIdb,
addDepth, addDepth,
deleteItemInCollection, deleteItemInCollection,
isItemARequest, isItemARequest,
isItemAFolder
} from 'utils/collections'; } from 'utils/collections';
import { parseQueryParams, stringifyQueryParams } from 'utils/url'; import { parseQueryParams, stringifyQueryParams } from 'utils/url';
import { getCollectionsFromIdb, saveCollectionToIdb } from 'utils/idb'; import { getCollectionsFromIdb, saveCollectionToIdb } from 'utils/idb';
import { each } from 'lodash';
// todo: errors should be tracked in each slice and displayed as toasts // todo: errors should be tracked in each slice and displayed as toasts
@ -75,6 +75,21 @@ export const collectionsSlice = createSlice({
} }
} }
}, },
_cloneItem: (state, action) => {
const collectionUid = action.payload.collectionUid;
const clonedItem = action.payload.clonedItem;
const parentItemUid = action.payload.parentItemUid;
const collection = findCollectionByUid(state.collections, collectionUid);
if(collection) {
if(parentItemUid) {
const parentItem = findItemInCollection(collection, parentItemUid);
parentItem.items.push(clonedItem);
} else {
collection.items.push(clonedItem);
}
}
},
_requestSent: (state, action) => { _requestSent: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -128,7 +143,7 @@ export const collectionsSlice = createSlice({
}, },
draft: null draft: null
}; };
item.draft = cloneItem(item); item.draft = cloneDeep(item);
collection.items.push(item); collection.items.push(item);
} }
}, },
@ -158,7 +173,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
item.draft.request.url = action.payload.url; item.draft.request.url = action.payload.url;
@ -194,7 +209,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
item.draft.request.params = item.draft.request.params || []; item.draft.request.params = item.draft.request.params || [];
item.draft.request.params.push({ item.draft.request.params.push({
@ -215,7 +230,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
const param = find(item.draft.request.params, (h) => h.uid === action.payload.param.uid); const param = find(item.draft.request.params, (h) => h.uid === action.payload.param.uid);
if(param) { if(param) {
@ -256,7 +271,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
item.draft.request.params = filter(item.draft.request.params, (p) => p.uid !== action.payload.paramUid); item.draft.request.params = filter(item.draft.request.params, (p) => p.uid !== action.payload.paramUid);
@ -279,7 +294,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
item.draft.request.headers = item.draft.request.headers || []; item.draft.request.headers = item.draft.request.headers || [];
item.draft.request.headers.push({ item.draft.request.headers.push({
@ -300,7 +315,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
const header = find(item.draft.request.headers, (h) => h.uid === action.payload.header.uid); const header = find(item.draft.request.headers, (h) => h.uid === action.payload.header.uid);
if(header) { if(header) {
@ -320,7 +335,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
item.draft.request.headers = filter(item.draft.request.headers, (h) => h.uid !== action.payload.headerUid); item.draft.request.headers = filter(item.draft.request.headers, (h) => h.uid !== action.payload.headerUid);
} }
@ -334,7 +349,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
item.draft.request.body = { item.draft.request.body = {
mode: action.payload.mode, mode: action.payload.mode,
@ -351,7 +366,7 @@ export const collectionsSlice = createSlice({
if(item && isItemARequest(item)) { if(item && isItemARequest(item)) {
if(!item.draft) { if(!item.draft) {
item.draft = cloneItem(item); item.draft = cloneDeep(item);
} }
item.draft.request.method = action.payload.method; item.draft.request.method = action.payload.method;
} }
@ -366,6 +381,7 @@ export const {
_newItem, _newItem,
_deleteItem, _deleteItem,
_renameItem, _renameItem,
_cloneItem,
_requestSent, _requestSent,
_responseReceived, _responseReceived,
_saveRequest, _saveRequest,
@ -584,6 +600,49 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
} }
}; };
export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if(collection) {
const collectionCopy = cloneDeep(collection);
const item = findItemInCollection(collectionCopy, itemUid);
if(!item) {
return;
}
if(isItemAFolder(item)) {
throw new Error('Cloning folders is not supported yet');
}
// 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);
saveCollectionToIdb(window.__idb, collectionToSave)
.then(() => {
dispatch(_cloneItem({
parentItemUid: parentItem ? parentItem.uid : null,
clonedItem: clonedItem,
collectionUid: collectionUid
}));
})
.catch((err) => console.log(err));
}
};
export const removeCollection = (collectionPath) => () => { export const removeCollection = (collectionPath) => () => {
console.log('removeCollection'); console.log('removeCollection');
}; };

View File

@ -73,16 +73,20 @@ export const findItemInCollection = (collection, itemUid) => {
return findItem(flattenedItems, itemUid); return findItem(flattenedItems, itemUid);
} }
export const findParentItemInCollection = (collection, itemUid) => {
let flattenedItems = flattenItems(collection.items);
return find(flattenedItems, (item) => {
return item.items && find(item.items, i => i.uid === itemUid);
});
}
export const recursivelyGetAllItemUids = (items = []) => { export const recursivelyGetAllItemUids = (items = []) => {
let flattenedItems = flattenItems(items); let flattenedItems = flattenItems(items);
return map(flattenedItems, (i) => i.uid); return map(flattenedItems, (i) => i.uid);
}; };
export const cloneItem = (item) => {
return cloneDeep(item);
};
export const transformCollectionToSaveToIdb = (collection, options = {}) => { export const transformCollectionToSaveToIdb = (collection, options = {}) => {
const copyHeaders = (headers) => { const copyHeaders = (headers) => {
return map(headers, (header) => { return map(headers, (header) => {
@ -172,7 +176,9 @@ export const deleteItemInCollection = (itemUid, collection) => {
}; };
export const isItemARequest = (item) => { export const isItemARequest = (item) => {
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type); return item.hasOwnProperty('request')
&& ['http-request', 'graphql-request'].includes(item.type)
&& !item.items;
}; };
export const isItemAFolder = (item) => { export const isItemAFolder = (item) => {