mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-24 17:03:47 +01:00
feat: clone request, resolves #3
This commit is contained in:
parent
c7cced5868
commit
85677b8a6c
@ -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;
|
@ -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);
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
};
|
};
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user