From e98f21944809d4b28a6e64d598c021c7252dea27 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Tue, 18 Oct 2022 03:39:36 +0530 Subject: [PATCH] feat: local collections environment sync --- .../App/useLocalCollectionTreeSync.js | 11 +- .../ReduxStore/slices/collections/actions.js | 103 +++++++++++++---- .../ReduxStore/slices/collections/index.js | 13 ++- packages/bruno-electron/src/app/watcher.js | 104 +++++++++++++++--- .../src/ipc/local-collection.js | 12 ++ .../bruno-schema/src/collections/index.js | 6 +- packages/bruno-schema/src/index.js | 3 +- 7 files changed, 209 insertions(+), 43 deletions(-) diff --git a/packages/bruno-app/src/providers/App/useLocalCollectionTreeSync.js b/packages/bruno-app/src/providers/App/useLocalCollectionTreeSync.js index 4c548464e..10f123af1 100644 --- a/packages/bruno-app/src/providers/App/useLocalCollectionTreeSync.js +++ b/packages/bruno-app/src/providers/App/useLocalCollectionTreeSync.js @@ -8,7 +8,10 @@ import { localCollectionUnlinkDirectoryEvent } from 'providers/ReduxStore/slices/collections'; import toast from 'react-hot-toast'; -import { openLocalCollectionEvent } from 'providers/ReduxStore/slices/collections/actions'; +import { + openLocalCollectionEvent, + localCollectionLoadEnvironmentsEvent +} from 'providers/ReduxStore/slices/collections/actions'; import { isElectron } from 'utils/common/platform'; const useLocalCollectionTreeSync = () => { @@ -54,6 +57,12 @@ const useLocalCollectionTreeSync = () => { directory: val })); } + if(type === 'addEnvironmentFile') { + dispatch(localCollectionLoadEnvironmentsEvent(val)); + } + if(type === 'changeEnvironmentFile') { + dispatch(localCollectionLoadEnvironmentsEvent(val)); + } }; const _collectionAlreadyOpened = (pathname) => { 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 e1e5e7abc..671d4bca2 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -18,7 +18,7 @@ import { refreshUidsInItem, interpolateEnvironmentVars } from 'utils/collections'; -import { collectionSchema, itemSchema } from '@usebruno/schema'; +import { collectionSchema, itemSchema, environmentsSchema } from '@usebruno/schema'; import { waitForNextTick } from 'utils/common'; import { getCollectionsFromIdb, saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb'; import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network'; @@ -41,6 +41,7 @@ import { createCollection as _createCollection, renameCollection as _renameCollection, deleteCollection as _deleteCollection, + localCollectionLoadEnvironmentsEvent as _localCollectionLoadEnvironmentsEvent } from './index'; import { closeTabs, addTab } from 'providers/ReduxStore/slices/tabs'; @@ -57,24 +58,6 @@ export const loadCollectionsFromIdb = () => (dispatch) => { .catch(() => toast.error("Error occured while loading collections from IndexedDB")); }; -export const openLocalCollectionEvent = (uid, pathname, name) => (dispatch, getState) => { - const localCollection = { - version: "1", - uid: uid, - name: name, - pathname: pathname, - items: [] - }; - - return new Promise((resolve, reject) => { - collectionSchema - .validate(localCollection) - .then(() => dispatch(_createCollection(localCollection))) - .then(resolve) - .catch(reject); - }); -}; - export const createCollection = (collectionName) => (dispatch, getState) => { const newCollection = { version: "1", @@ -659,6 +642,15 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => collectionToSave.environments = collectionToSave.environments || []; collectionToSave.environments.push(environment); + if(isLocalCollection(collection)) { + environmentsSchema + .validate(collectionToSave.environments) + .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, collectionToSave.environments)) + .then(resolve) + .catch(reject); + return; + } + collectionSchema .validate(collectionToSave) .then(() => saveCollectionToIdb(window.__idb, collectionToSave)) @@ -683,9 +675,18 @@ export const renameEnvironment = (newName, environmentUid, collectionUid) => (d } environment.name = newName; - const collectionToSave = transformCollectionToSaveToIdb(collectionCopy); + if(isLocalCollection(collection)) { + const environments = collectionToSave.environments; + environmentsSchema + .validate(environments) + .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environments)) + .then(resolve) + .catch(reject); + return; + } + collectionSchema .validate(collectionToSave) .then(() => saveCollectionToIdb(window.__idb, collectionToSave)) @@ -704,15 +705,25 @@ export const deleteEnvironment = (environmentUid, collectionUid) => (dispatch, } const collectionCopy = cloneDeep(collection); + const environment = findEnvironmentInCollection(collectionCopy, environmentUid); if(!environment) { return reject(new Error('Environment not found')); } collectionCopy.environments = filter(collectionCopy.environments, (e) => e.uid !== environmentUid); - const collectionToSave = transformCollectionToSaveToIdb(collectionCopy); + if(isLocalCollection(collection)) { + const environments = collectionToSave.environments; + environmentsSchema + .validate(environments) + .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environments)) + .then(resolve) + .catch(reject); + return; + } + collectionSchema .validate(collectionToSave) .then(() => saveCollectionToIdb(window.__idb, collectionToSave)) @@ -739,6 +750,15 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di environment.variables = variables; const collectionToSave = transformCollectionToSaveToIdb(collectionCopy); + if(isLocalCollection(collection)) { + const environments = collectionToSave.environments; + environmentsSchema + .validate(environments) + .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environments)) + .then(resolve) + .catch(reject); + return; + } collectionSchema .validate(collectionToSave) @@ -814,6 +834,24 @@ export const browserLocalDirectory = () => (dispatch, getState) => { }); } +export const openLocalCollectionEvent = (uid, pathname, name) => (dispatch, getState) => { + const localCollection = { + version: "1", + uid: uid, + name: name, + pathname: pathname, + items: [] + }; + + return new Promise((resolve, reject) => { + collectionSchema + .validate(localCollection) + .then(() => dispatch(_createCollection(localCollection))) + .then(resolve) + .catch(reject); + }); +}; + export const createLocalCollection = (collectionName, collectionLocation) => () => { const { ipcRenderer } = window; @@ -834,4 +872,25 @@ export const openLocalCollection = () => () => { .then(resolve) .catch(reject); }); -}; \ No newline at end of file +}; + +export const localCollectionLoadEnvironmentsEvent = (payload) => (dispatch, getState) => { + const { data: environments, meta } = payload; + + return new Promise((resolve, reject) => { + const state = getState(); + const collection = findCollectionByUid(state.collections.collections, meta.collectionUid); + if(!collection) { + return reject(new Error('Collection not found')); + } + + environmentsSchema + .validate(environments) + .then(() => dispatch(_localCollectionLoadEnvironmentsEvent({ + environments, + collectionUid: meta.collectionUid + }))) + .then(resolve) + .catch(reject); + }); +}; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 8bb274586..8d1daac50 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -742,7 +742,15 @@ export const collectionsSlice = createSlice({ deleteItemInCollection(item.uid, collection); } } - } + }, + localCollectionLoadEnvironmentsEvent: (state, action) => { + const { environments, collectionUid } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + + if(collection) { + collection.environments = environments; + } + }, } }); @@ -788,7 +796,8 @@ export const { localCollectionAddDirectoryEvent, localCollectionChangeFileEvent, localCollectionUnlinkFileEvent, - localCollectionUnlinkDirectoryEvent + localCollectionUnlinkDirectoryEvent, + localCollectionLoadEnvironmentsEvent } = collectionsSlice.actions; export default collectionsSlice.reducer; diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index ae5e4bf7a..58340aec7 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -3,11 +3,75 @@ const path = require('path'); const chokidar = require('chokidar'); const { hasJsonExtension } = require('../utils/filesystem'); -const add = async (win, pathname, collectionUid) => { +const isEnvironmentConfig = (pathname, collectionPath) => { + const dirname = path.dirname(pathname); + const basename = path.basename(pathname); + + return dirname === collectionPath && basename === 'environments.json'; +} + +const addEnvironmentFile = async (win, pathname, collectionUid) => { + try { + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname), + } + }; + + const jsonData = fs.readFileSync(pathname, 'utf8'); + file.data = JSON.parse(jsonData); + win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file); + } catch (err) { + console.error(err) + } +}; + +const changeEnvironmentFile = async (win, pathname, collectionUid) => { + try { + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname), + } + }; + + const jsonData = fs.readFileSync(pathname, 'utf8'); + file.data = JSON.parse(jsonData); + win.webContents.send('main:collection-tree-updated', 'changeEnvironmentFile', file); + } catch (err) { + console.error(err) + } +}; + +const unlinkEnvironmentFile = async (win, pathname, collectionUid) => { + try { + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname), + }, + data: [] + }; + + win.webContents.send('main:collection-tree-updated', 'changeEnvironmentFile', file); + } catch (err) { + console.error(err) + } +}; + +const add = async (win, pathname, collectionUid, collectionPath) => { const isJson = hasJsonExtension(pathname); console.log(`watcher add: ${pathname}`); if(isJson) { + if(isEnvironmentConfig(pathname, collectionPath)) { + return addEnvironmentFile(win, pathname, collectionUid); + } + const file = { meta: { collectionUid, @@ -38,17 +102,21 @@ const addDirectory = (win, pathname, collectionUid) => { win.webContents.send('main:collection-tree-updated', 'addDir', directory); }; -const change = async (win, pathname, collectionUid) => { +const change = async (win, pathname, collectionUid, collectionPath) => { console.log(`watcher change: ${pathname}`); - const file = { - meta: { - collectionUid, - pathname, - name: path.basename(pathname), - } - }; - try { + if(isEnvironmentConfig(pathname, collectionPath)) { + return changeEnvironmentFile(win, pathname, collectionUid); + } + + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname), + } + }; + const jsonData = fs.readFileSync(pathname, 'utf8'); file.data = await JSON.parse(jsonData); win.webContents.send('main:collection-tree-updated', 'change', file); @@ -57,7 +125,11 @@ const change = async (win, pathname, collectionUid) => { } }; -const unlink = (win, pathname, collectionUid) => { +const unlink = (win, pathname, collectionUid, collectionPath) => { + if(isEnvironmentConfig(pathname, collectionPath)) { + return unlinkEnvironmentFile(win, pathname, collectionUid); + } + console.log(`watcher unlink: ${pathname}`); const file = { meta: { @@ -107,11 +179,11 @@ class Watcher { }); watcher - .on('add', pathname => add(win, pathname, collectionUid)) - .on('addDir', pathname => addDirectory(win, pathname, collectionUid)) - .on('change', pathname => change(win, pathname, collectionUid)) - .on('unlink', pathname => unlink(win, pathname, collectionUid)) - .on('unlinkDir', pathname => unlinkDir(win, pathname, collectionUid)) + .on('add', pathname => add(win, pathname, collectionUid, watchPath)) + .on('addDir', pathname => addDirectory(win, pathname, collectionUid, watchPath)) + .on('change', pathname => change(win, pathname, collectionUid, watchPath)) + .on('unlink', pathname => unlink(win, pathname, collectionUid, watchPath)) + .on('unlinkDir', pathname => unlinkDir(win, pathname, collectionUid, watchPath)) self.watchers[watchPath] = watcher; }, 100); diff --git a/packages/bruno-electron/src/ipc/local-collection.js b/packages/bruno-electron/src/ipc/local-collection.js index a8d8695e7..94f575c5e 100644 --- a/packages/bruno-electron/src/ipc/local-collection.js +++ b/packages/bruno-electron/src/ipc/local-collection.js @@ -85,6 +85,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + // save environment + ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environments) => { + try { + const envFilePath = path.join(collectionPathname, 'environments.json'); + + const content = await stringifyJson(environments); + await writeFile(envFilePath, content); + } catch (error) { + return Promise.reject(error); + } + }); + // rename item ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => { try { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index da680a6ab..60326de7d 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -16,6 +16,8 @@ const environmentSchema = Yup.object({ variables: Yup.array().of(environmentVariablesSchema).required('variables are required') }).noUnknown(true).strict(); +const environmentsSchema = Yup.array().of(environmentSchema); + const keyValueSchema = Yup.object({ uid: uidSchema, name: Yup.string().nullable().max(256, 'name must be 256 characters or less'), @@ -75,12 +77,14 @@ const collectionSchema = Yup.object({ .length(21, 'activeEnvironmentUid must be 21 characters in length') .matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric') .nullable(), - environments: Yup.array().of(environmentSchema), + environments: environmentsSchema, pathname: Yup.string().max(1024, 'pathname cannot be more than 1024 characters').nullable() }).noUnknown(true).strict(); + module.exports = { requestSchema, itemSchema, + environmentsSchema, collectionSchema }; \ No newline at end of file diff --git a/packages/bruno-schema/src/index.js b/packages/bruno-schema/src/index.js index 86dcf272b..bfede8235 100644 --- a/packages/bruno-schema/src/index.js +++ b/packages/bruno-schema/src/index.js @@ -1,8 +1,9 @@ const { workspaceSchema } = require("./workspaces"); -const { collectionSchema, itemSchema } = require("./collections"); +const { collectionSchema, itemSchema, environmentsSchema } = require("./collections"); module.exports = { itemSchema, + environmentsSchema, collectionSchema, workspaceSchema }; \ No newline at end of file