diff --git a/renderer/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/renderer/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js new file mode 100644 index 000000000..d71ed3e22 --- /dev/null +++ b/renderer/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js @@ -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 ( + +
+
+ + + {formik.touched.name && formik.errors.name ? ( +
{formik.errors.name}
+ ) : null} +
+
+
+ ); +}; + +export default CloneCollectionItem; diff --git a/renderer/components/Sidebar/Collections/Collection/CollectionItem/index.js b/renderer/components/Sidebar/Collections/Collection/CollectionItem/index.js index f9b67f259..66ad65486 100644 --- a/renderer/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/renderer/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -10,6 +10,7 @@ import NewRequest from 'components/Sidebar/NewRequest'; import NewFolder from 'components/Sidebar/NewFolder'; import RequestMethod from './RequestMethod'; import RenameCollectionItem from './RenameCollectionItem'; +import CloneCollectionItem from './CloneCollectionItem'; import DeleteCollectionItem from './DeleteCollectionItem'; import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs'; @@ -22,6 +23,7 @@ const CollectionItem = ({item, collection}) => { const dispatch = useDispatch(); const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); + const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false); const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false); const [newRequestModalOpen, setNewRequestModalOpen] = useState(false); const [newFolderModalOpen, setNewFolderModalOpen] = useState(false); @@ -74,6 +76,7 @@ const CollectionItem = ({item, collection}) => { return ( {renameItemModalOpen && setRenameItemModalOpen(false)}/>} + {cloneItemModalOpen && setCloneItemModalOpen(false)}/>} {deleteItemModalOpen && setDeleteItemModalOpen(false)}/>} {newRequestModalOpen && setNewRequestModalOpen(false)}/>} {newFolderModalOpen && setNewFolderModalOpen(false)}/>} @@ -137,6 +140,14 @@ const CollectionItem = ({item, collection}) => { }}> Rename + {!isFolder && ( +
{ + dropdownTippyRef.current.hide(); + setCloneItemModalOpen(true); + }}> + Clone +
+ )}
{ dropdownTippyRef.current.hide(); setDeleteItemModalOpen(true); diff --git a/renderer/components/Sidebar/Collections/index.js b/renderer/components/Sidebar/Collections/index.js index 951a0e809..c5e0c8890 100644 --- a/renderer/components/Sidebar/Collections/index.js +++ b/renderer/components/Sidebar/Collections/index.js @@ -4,6 +4,7 @@ import Collection from './Collection'; const Collections = () => { const collections = useSelector((state) => state.collections.collections); + console.log(collections); return (
diff --git a/renderer/globalStyles.js b/renderer/globalStyles.js index d36372eb8..cd18287c6 100644 --- a/renderer/globalStyles.js +++ b/renderer/globalStyles.js @@ -6,7 +6,7 @@ const GlobalStyle = createGlobalStyle` border-right: solid 1px var(--color-codemirror-border); } - .grafnode-form { + .bruno-form { .textbox { line-height: 1.42857143; background-color: #fff; diff --git a/renderer/providers/ReduxStore/slices/collections.js b/renderer/providers/ReduxStore/slices/collections.js index 524d5fb64..9c63b952f 100644 --- a/renderer/providers/ReduxStore/slices/collections.js +++ b/renderer/providers/ReduxStore/slices/collections.js @@ -1,9 +1,9 @@ import path from 'path'; import { uuid } from 'utils/common'; -import trim from 'lodash/trim'; import find from 'lodash/find'; import concat from 'lodash/concat'; import filter from 'lodash/filter'; +import each from 'lodash/each'; import cloneDeep from 'lodash/cloneDeep'; import { createSlice } from '@reduxjs/toolkit' import splitOnFirst from 'split-on-first'; @@ -11,15 +11,15 @@ import { sendNetworkRequest } from 'utils/network'; import { findCollectionByUid, findItemInCollection, - cloneItem, + findParentItemInCollection, transformCollectionToSaveToIdb, addDepth, deleteItemInCollection, isItemARequest, + isItemAFolder } from 'utils/collections'; import { parseQueryParams, stringifyQueryParams } from 'utils/url'; import { getCollectionsFromIdb, saveCollectionToIdb } from 'utils/idb'; -import { each } from 'lodash'; // todo: errors should be tracked in each slice and displayed as toasts @@ -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) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -128,7 +143,7 @@ export const collectionsSlice = createSlice({ }, draft: null }; - item.draft = cloneItem(item); + item.draft = cloneDeep(item); collection.items.push(item); } }, @@ -158,7 +173,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { if(!item.draft) { - item.draft = cloneItem(item); + item.draft = cloneDeep(item); } item.draft.request.url = action.payload.url; @@ -194,7 +209,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { if(!item.draft) { - item.draft = cloneItem(item); + item.draft = cloneDeep(item); } item.draft.request.params = item.draft.request.params || []; item.draft.request.params.push({ @@ -215,7 +230,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { 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); if(param) { @@ -256,7 +271,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { 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); @@ -279,7 +294,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { if(!item.draft) { - item.draft = cloneItem(item); + item.draft = cloneDeep(item); } item.draft.request.headers = item.draft.request.headers || []; item.draft.request.headers.push({ @@ -300,7 +315,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { 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); if(header) { @@ -320,7 +335,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { 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); } @@ -334,7 +349,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { if(!item.draft) { - item.draft = cloneItem(item); + item.draft = cloneDeep(item); } item.draft.request.body = { mode: action.payload.mode, @@ -351,7 +366,7 @@ export const collectionsSlice = createSlice({ if(item && isItemARequest(item)) { if(!item.draft) { - item.draft = cloneItem(item); + item.draft = cloneDeep(item); } item.draft.request.method = action.payload.method; } @@ -366,6 +381,7 @@ export const { _newItem, _deleteItem, _renameItem, + _cloneItem, _requestSent, _responseReceived, _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) => () => { console.log('removeCollection'); }; diff --git a/renderer/utils/collections/index.js b/renderer/utils/collections/index.js index 53b6b052f..ac1e403de 100644 --- a/renderer/utils/collections/index.js +++ b/renderer/utils/collections/index.js @@ -73,16 +73,20 @@ export const findItemInCollection = (collection, 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 = []) => { let flattenedItems = flattenItems(items); return map(flattenedItems, (i) => i.uid); }; -export const cloneItem = (item) => { - return cloneDeep(item); -}; - export const transformCollectionToSaveToIdb = (collection, options = {}) => { const copyHeaders = (headers) => { return map(headers, (header) => { @@ -172,7 +176,9 @@ export const deleteItemInCollection = (itemUid, collection) => { }; 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) => {