diff --git a/package-lock.json b/package-lock.json index f09a7bcdb..5c72b876d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14315,6 +14315,7 @@ }, "node_modules/react-is": { "version": "18.2.0", + "dev": true, "license": "MIT" }, "node_modules/react-pdf": { @@ -21435,8 +21436,7 @@ } }, "@tabler/icons": { - "version": "1.119.0", - "requires": {} + "version": "1.119.0" }, "@tippyjs/react": { "version": "4.2.6", @@ -21989,8 +21989,7 @@ } }, "@usebruno/schema": { - "version": "file:packages/bruno-schema", - "requires": {} + "version": "file:packages/bruno-schema" }, "@usebruno/testbench": { "version": "file:packages/bruno-testbench", @@ -22134,8 +22133,7 @@ }, "@webpack-cli/configtest": { "version": "1.2.0", - "dev": true, - "requires": {} + "dev": true }, "@webpack-cli/info": { "version": "1.5.0", @@ -22146,8 +22144,7 @@ }, "@webpack-cli/serve": { "version": "1.7.0", - "dev": true, - "requires": {} + "dev": true }, "@xtuc/ieee754": { "version": "1.2.0", @@ -22229,8 +22226,7 @@ }, "ajv-keywords": { "version": "3.5.2", - "dev": true, - "requires": {} + "dev": true }, "amdefine": { "version": "0.0.8" @@ -23179,8 +23175,7 @@ "chai-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.5.0.tgz", - "integrity": "sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw==", - "requires": {} + "integrity": "sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw==" }, "chalk": { "version": "4.1.2", @@ -23615,8 +23610,7 @@ }, "css-declaration-sorter": { "version": "6.3.1", - "dev": true, - "requires": {} + "dev": true }, "css-loader": { "version": "6.7.3", @@ -23735,8 +23729,7 @@ }, "cssnano-utils": { "version": "3.1.0", - "dev": true, - "requires": {} + "dev": true }, "csso": { "version": "4.2.0", @@ -24963,8 +24956,7 @@ } }, "goober": { - "version": "2.1.11", - "requires": {} + "version": "2.1.11" }, "gopd": { "version": "1.0.1", @@ -25358,8 +25350,7 @@ }, "icss-utils": { "version": "5.1.0", - "dev": true, - "requires": {} + "dev": true }, "idb": { "version": "7.1.1" @@ -25959,8 +25950,7 @@ }, "jest-pnp-resolver": { "version": "1.2.3", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "29.2.0", @@ -26679,8 +26669,7 @@ "merge-refs": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", - "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==", - "requires": {} + "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==" }, "merge-stream": { "version": "2.0.0", @@ -26690,8 +26679,7 @@ "version": "1.4.1" }, "meros": { - "version": "1.2.1", - "requires": {} + "version": "1.2.1" }, "methods": { "version": "1.1.2" @@ -27568,23 +27556,19 @@ }, "postcss-discard-comments": { "version": "5.1.2", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-duplicates": { "version": "5.1.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-empty": { "version": "5.1.1", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-overridden": { "version": "5.1.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-js": { "version": "3.0.3", @@ -27666,8 +27650,7 @@ }, "postcss-modules-extract-imports": { "version": "3.0.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -27700,8 +27683,7 @@ }, "postcss-normalize-charset": { "version": "5.1.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -28186,11 +28168,11 @@ } }, "react-inspector": { - "version": "6.0.2", - "requires": {} + "version": "6.0.2" }, "react-is": { - "version": "18.2.0" + "version": "18.2.0", + "dev": true }, "react-pdf": { "version": "7.5.1", @@ -28352,8 +28334,7 @@ } }, "redux-thunk": { - "version": "2.4.2", - "requires": {} + "version": "2.4.2" }, "regenerate": { "version": "1.4.2", @@ -28591,8 +28572,7 @@ }, "rollup-plugin-peer-deps-external": { "version": "2.2.4", - "dev": true, - "requires": {} + "dev": true }, "rollup-plugin-postcss": { "version": "4.0.2", @@ -29152,8 +29132,7 @@ }, "style-loader": { "version": "3.3.1", - "dev": true, - "requires": {} + "dev": true }, "styled-components": { "version": "5.3.6", @@ -29182,8 +29161,7 @@ } }, "styled-jsx": { - "version": "5.0.7", - "requires": {} + "version": "5.0.7" }, "stylehacks": { "version": "5.1.1", @@ -29856,8 +29834,7 @@ } }, "use-sync-external-store": { - "version": "1.2.0", - "requires": {} + "version": "1.2.0" }, "utf8-byte-length": { "version": "1.0.4", @@ -30018,8 +29995,7 @@ }, "acorn-import-assertions": { "version": "1.8.0", - "dev": true, - "requires": {} + "dev": true }, "schema-utils": { "version": "3.1.1", diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js new file mode 100644 index 000000000..cd9857a15 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js @@ -0,0 +1,155 @@ +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 { cloneCollection } from 'providers/ReduxStore/slices/collections/actions'; +import toast from 'react-hot-toast'; +import Tooltip from 'components/Tooltip'; +import Modal from 'components/Modal'; + +const CloneCollection = ({ onClose, collection }) => { + const inputRef = useRef(); + const dispatch = useDispatch(); + + const formik = useFormik({ + enableReinitialize: true, + initialValues: { + collectionName: '', + collectionFolderName: '', + collectionLocation: '' + }, + validationSchema: Yup.object({ + collectionName: Yup.string() + .min(1, 'must be at least 1 character') + .max(50, 'must be 50 characters or less') + .required('collection name is required'), + collectionFolderName: Yup.string() + .min(1, 'must be at least 1 character') + .max(50, 'must be 50 characters or less') + .matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters') + .required('folder name is required'), + collectionLocation: Yup.string().min(1, 'location is required').required('location is required') + }), + onSubmit: (values) => { + dispatch( + cloneCollection( + values.collectionName, + values.collectionFolderName, + values.collectionLocation, + collection.pathname + ) + ) + .then(() => { + toast.success('Collection created'); + onClose(); + }) + .catch(() => toast.error('An error occurred while creating the collection')); + } + }); + + const browse = () => { + dispatch(browseDirectory()) + .then((dirPath) => { + // When the user closes the diolog without selecting anything dirPath will be false + if (typeof dirPath === 'string') { + 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 ( + +
+
+ + { + formik.handleChange(e); + if (formik.values.collectionName === formik.values.collectionFolderName) { + formik.setFieldValue('collectionFolderName', e.target.value); + } + }} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + value={formik.values.collectionName || ''} + /> + {formik.touched.collectionName && formik.errors.collectionName ? ( +
{formik.errors.collectionName}
+ ) : null} + + + + {formik.touched.collectionLocation && formik.errors.collectionLocation ? ( +
{formik.errors.collectionLocation}
+ ) : null} +
+ + Browse + +
+ + + + {formik.touched.collectionFolderName && formik.errors.collectionFolderName ? ( +
{formik.errors.collectionFolderName}
+ ) : null} +
+
+
+ ); +}; + +export default CloneCollection; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index dade095ed..81502831d 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -20,11 +20,13 @@ import exportCollection from 'utils/collections/export'; import RenameCollection from './RenameCollection'; import StyledWrapper from './StyledWrapper'; +import CloneCollection from './CloneCollection/index'; const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); const [showNewRequestModal, setShowNewRequestModal] = useState(false); const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false); + const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false); const [showExportCollectionModal, setShowExportCollectionModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed); @@ -133,6 +135,9 @@ const Collection = ({ collection, searchText }) => { {showExportCollectionModal && ( setShowExportCollectionModal(false)} /> )} + {showCloneCollectionModalOpen && ( + setShowCloneCollectionModalOpen(false)} /> + )}
{ > New Folder
+
{ + menuDropdownTippyRef.current.hide(); + setShowCloneCollectionModalOpen(true); + }} + > + Clone +
{ diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index c7bb98378..9c799c234 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -939,7 +939,17 @@ export const createCollection = (collectionName, collectionFolderName, collectio .catch(reject); }); }; +export const cloneCollection = (collectionName, collectionFolderName, collectionLocation, perviousPath) => () => { + const { ipcRenderer } = window; + return ipcRenderer.invoke( + 'renderer:clone-collection', + collectionName, + collectionFolderName, + collectionLocation, + perviousPath + ); +}; export const openCollection = () => () => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index ba19cfaf2..76bb661d8 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -70,7 +70,52 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } } ); + // clone collection + ipcMain.handle( + 'renderer:clone-collection', + async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => { + const dirPath = path.join(collectionLocation, collectionFolderName); + if (fs.existsSync(dirPath)) { + throw new Error(`collection: ${dirPath} already exists`); + } + if (!isValidPathname(dirPath)) { + throw new Error(`collection: invalid pathname - ${dir}`); + } + + // create dir + await createDirectory(dirPath); + const uid = generateUidBasedOnHash(dirPath); + + // open the bruno.json of previousPath + const brunoJsonFilePath = path.join(previousPath, 'bruno.json'); + const content = fs.readFileSync(brunoJsonFilePath, 'utf8'); + + //Change new name of collection + let json = JSON.parse(content); + json.name = collectionName; + const cont = await stringifyJson(json); + + // write the bruno.json to new dir + await writeFile(path.join(dirPath, 'bruno.json'), cont); + + // Now copy all the files with extension name .bru along with there dir + const files = searchForBruFiles(previousPath); + + for (const sourceFilePath of files) { + const relativePath = path.relative(previousPath, sourceFilePath); + const newFilePath = path.join(dirPath, relativePath); + + // handle dir of files + fs.mkdirSync(path.dirname(newFilePath), { recursive: true }); + // copy each files + fs.copyFileSync(sourceFilePath, newFilePath); + } + + mainWindow.webContents.send('main:collection-opened', dirPath, uid, json); + ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid); + } + ); // rename collection ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => { try {