diff --git a/packages/bruno-app/src/providers/App/useLocalCollectionTreeSync.js b/packages/bruno-app/src/providers/App/useLocalCollectionTreeSync.js index ac4f73404..4c548464e 100644 --- a/packages/bruno-app/src/providers/App/useLocalCollectionTreeSync.js +++ b/packages/bruno-app/src/providers/App/useLocalCollectionTreeSync.js @@ -21,9 +21,9 @@ const useLocalCollectionTreeSync = () => { const { ipcRenderer } = window; - const _openCollection = (pathname, uid) => { - console.log(`collection uid: ${uid}, pathname: ${pathname}`); - dispatch(openLocalCollectionEvent(uid, pathname)); + const _openCollection = (pathname, uid, name) => { + console.log(`collection uid: ${uid}, pathname: ${pathname}, name: ${name}`); + dispatch(openLocalCollectionEvent(uid, pathname, name)); }; const _collectionTreeUpdated = (type, val) => { @@ -60,16 +60,22 @@ const useLocalCollectionTreeSync = () => { toast.success('Collection is already opened under local collections'); }; + const _displayError = (message) => { + toast.error(message || 'Something went wrong!'); + }; + ipcRenderer.invoke('renderer:ready'); const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection); const removeListener2 = ipcRenderer.on('main:collection-tree-updated', _collectionTreeUpdated); const removeListener3 = ipcRenderer.on('main:collection-already-opened', _collectionAlreadyOpened); + const removeListener4 = ipcRenderer.on('main:display-error', _displayError); return () => { removeListener1(); removeListener2(); removeListener3(); + removeListener4(); }; }, [isElectron]); }; 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 8fd4ce7c2..e1e5e7abc 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -57,11 +57,11 @@ export const loadCollectionsFromIdb = () => (dispatch) => { .catch(() => toast.error("Error occured while loading collections from IndexedDB")); }; -export const openLocalCollectionEvent = (uid, pathname) => (dispatch, getState) => { +export const openLocalCollectionEvent = (uid, pathname, name) => (dispatch, getState) => { const localCollection = { version: "1", uid: uid, - name: path.basename(pathname), + name: name, pathname: pathname, items: [] }; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 9a7a5b9b5..532822474 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -22,7 +22,8 @@ "fs-extra": "^10.1.0", "is-valid-path": "^0.1.1", "lodash": "^4.17.21", - "nanoid": "3.3.4" + "nanoid": "3.3.4", + "yup": "^0.32.11" }, "devDependencies": { "electron": "^21.1.1", diff --git a/packages/bruno-electron/src/app/collections.js b/packages/bruno-electron/src/app/collections.js index da649eb21..8a022a177 100644 --- a/packages/bruno-electron/src/app/collections.js +++ b/packages/bruno-electron/src/app/collections.js @@ -1,8 +1,52 @@ -const { uuid } = require('../utils/common'); +const fs = require('fs'); +const path = require('path'); const { dialog, ipcMain } = require('electron'); +const Yup = require('yup'); const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem'); -const openCollection = async (win, watcher) => { +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(); + +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') +}).noUnknown(true).strict(); + +const readConfigFile = async (pathname) => { + try { + const jsonData = fs.readFileSync(pathname, 'utf8'); + return JSON.parse(jsonData); + } catch(err) { + return Promise.reject(new Error("Unable to parse json in bruno.json")); + } +} + +const validateSchema = async (config) => { + try { + await configSchema.validate(config); + } catch(err) { + return Promise.reject(new Error("bruno.json format is invalid")); + } +}; + +const getCollectionConfigFile = async (pathname) => { + const configFilePath = path.join(pathname, 'bruno.json'); + if (!fs.existsSync(configFilePath)){ + throw new Error(`The collection is not valid (bruno.json not found)`); + } + + const config = await readConfigFile(configFilePath); + await validateSchema(config); + + return config; +} + +const openCollectionDialog = async (win, watcher) => { const { filePaths } = await dialog.showOpenDialog(win, { properties: ['openDirectory', 'createDirectory'] }); @@ -10,19 +54,37 @@ const openCollection = async (win, watcher) => { if (filePaths && filePaths[0]) { const resolvedPath = normalizeAndResolvePath(filePaths[0]); if (isDirectory(resolvedPath)) { - if(!watcher.hasWatcher(resolvedPath)) { - const uid = uuid(); - win.webContents.send('main:collection-opened', resolvedPath, uid); - ipcMain.emit('main:collection-opened', win, resolvedPath, uid); - } else { - win.webContents.send('main:collection-already-opened', resolvedPath); - } + openCollection(win, watcher, resolvedPath); } else { console.error(`[ERROR] Cannot open unknown folder: "${resolvedPath}"`); } } +} + +const openCollection = async (win, watcher, collectionPath, options = {}) => { + if(!watcher.hasWatcher(collectionPath)) { + try { + const { + uid, + name + } = await getCollectionConfigFile(collectionPath); + + console.log(uid); + console.log(name); + + win.webContents.send('main:collection-opened', collectionPath, uid, name); + ipcMain.emit('main:collection-opened', win, collectionPath, uid); + } catch(err) { + if(!options.dontSendDisplayErrors) { + win.webContents.send('main:display-error', err.message || 'An error occured while opening the local collection'); + } + } + } else { + win.webContents.send('main:collection-already-opened', collectionPath); + } }; module.exports = { - openCollection + openCollection, + openCollectionDialog }; diff --git a/packages/bruno-electron/src/ipc/local-collection.js b/packages/bruno-electron/src/ipc/local-collection.js index 819623089..a8d8695e7 100644 --- a/packages/bruno-electron/src/ipc/local-collection.js +++ b/packages/bruno-electron/src/ipc/local-collection.js @@ -11,7 +11,7 @@ const { createDirectory } = require('../utils/filesystem'); const { uuid, stringifyJson, parseJson } = require('../utils/common'); -const { openCollection } = require('../app/collections'); +const { openCollectionDialog, openCollection } = require('../app/collections'); const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { // browse directory @@ -41,14 +41,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const uid = uuid(); const content = await stringifyJson({ - version: '1.0', + version: '1', uid: uid, name: collectionName, type: 'collection' }); await writeFile(path.join(dirPath, 'bruno.json'), content); - mainWindow.webContents.send('main:collection-opened', dirPath, uid); + mainWindow.webContents.send('main:collection-opened', dirPath, uid, collectionName); ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid); return; @@ -149,7 +149,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:open-collection', () => { if(watcher && mainWindow) { - openCollection(mainWindow, watcher); + openCollectionDialog(mainWindow, watcher); } }); @@ -168,9 +168,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if(lastOpened && lastOpened.length) { for(let collectionPath of lastOpened) { if(isDirectory(collectionPath)) { - const uid = uuid(); - mainWindow.webContents.send('main:collection-opened', collectionPath, uid); - ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid); + openCollection(mainWindow, watcher, collectionPath, { + dontSendDisplayErrors: true + }); } } } @@ -180,7 +180,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { ipcMain.on('main:open-collection', () => { if(watcher && mainWindow) { - openCollection(mainWindow, watcher); + openCollectionDialog(mainWindow, watcher); } });