From 97206b4d186bb680015eca476cb259e401d5e00e Mon Sep 17 00:00:00 2001 From: pyphilia Date: Fri, 1 May 2020 13:35:07 +0200 Subject: [PATCH] feat: add selection when loading a space, multiple refactor --- public/app/config/channels.js | 4 +- public/app/listeners/index.js | 8 +- public/app/listeners/loadSpace.js | 144 ++++++--- public/electron.js | 19 +- src/App.js | 7 + src/actions/index.js | 1 + src/actions/loadSpace.js | 107 +++++++ src/actions/space.js | 84 ------ src/actions/syncSpace.js | 51 +++- src/components/LoadSpace.js | 34 ++- .../space/export/ExportSelectionScreen.js | 59 +++- .../space/load/LoadSelectionScreen.js | 273 ++++++++++++++++++ src/config/channels.js | 4 +- src/config/paths.js | 1 + src/reducers/SpaceReducer.js | 2 - src/reducers/index.js | 2 + src/reducers/loadSpaceReducer.js | 44 +++ src/types/index.js | 1 + src/types/loadSpace.js | 9 + src/types/space.js | 1 - 20 files changed, 691 insertions(+), 164 deletions(-) create mode 100644 src/actions/loadSpace.js create mode 100644 src/components/space/load/LoadSelectionScreen.js create mode 100644 src/reducers/loadSpaceReducer.js create mode 100644 src/types/loadSpace.js diff --git a/public/app/config/channels.js b/public/app/config/channels.js index f47ab1f0..fa56e3ad 100644 --- a/public/app/config/channels.js +++ b/public/app/config/channels.js @@ -8,14 +8,16 @@ module.exports = { GET_SPACES_CHANNEL: 'spaces:get', DELETE_SPACE_CHANNEL: 'space:delete', DELETED_SPACE_CHANNEL: 'space:deleted', + CANCEL_LOAD_SPACE_CHANNEL: 'space:load:cancel', + EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL: 'space:load:extract-file', LOAD_SPACE_CHANNEL: 'space:load', LOADED_SPACE_CHANNEL: 'space:loaded', EXPORT_SPACE_CHANNEL: 'space:export', EXPORTED_SPACE_CHANNEL: 'space:exported', SHOW_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:show', SHOW_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:show', - RESPOND_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:response', RESPOND_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:respond', + RESPOND_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:respond', SHOW_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:show', RESPOND_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:respond', GET_USER_FOLDER_CHANNEL: 'user:folder:get', diff --git a/public/app/listeners/index.js b/public/app/listeners/index.js index ef7d651a..259e165a 100644 --- a/public/app/listeners/index.js +++ b/public/app/listeners/index.js @@ -1,4 +1,8 @@ -const loadSpace = require('./loadSpace'); +const { + loadSpace, + cancelLoadSpace, + extractFileToLoadSpace, +} = require('./loadSpace'); const saveSpace = require('./saveSpace'); const getSpace = require('./getSpace'); const getSpaces = require('./getSpaces'); @@ -34,6 +38,8 @@ const setSpaceAsRecent = require('./setSpaceAsRecent'); module.exports = { loadSpace, + cancelLoadSpace, + extractFileToLoadSpace, saveSpace, getSpace, getSpaces, diff --git a/public/app/listeners/loadSpace.js b/public/app/listeners/loadSpace.js index 71de9a2c..b5c2dda3 100644 --- a/public/app/listeners/loadSpace.js +++ b/public/app/listeners/loadSpace.js @@ -1,14 +1,15 @@ const extract = require('extract-zip'); +const _ = require('lodash'); const { promisify } = require('util'); const fs = require('fs'); const ObjectId = require('bson-objectid'); const { VAR_FOLDER } = require('../config/config'); -const { LOADED_SPACE_CHANNEL } = require('../config/channels'); const { - ERROR_SPACE_ALREADY_AVAILABLE, - ERROR_GENERAL, - ERROR_ZIP_CORRUPTED, -} = require('../config/errors'); + LOADED_SPACE_CHANNEL, + CANCEL_LOAD_SPACE_CHANNEL, + EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL, +} = require('../config/channels'); +const { ERROR_GENERAL, ERROR_ZIP_CORRUPTED } = require('../config/errors'); const logger = require('../logger'); const { performFileSystemOperation, @@ -24,7 +25,10 @@ const { // use promisified fs const fsPromises = fs.promises; -const loadSpace = (mainWindow, db) => async (event, { fileLocation }) => { +const extractFileToLoadSpace = (mainWindow, db) => async ( + event, + { fileLocation } +) => { const tmpId = ObjectId().str; // make temporary folder hidden @@ -37,7 +41,10 @@ const loadSpace = (mainWindow, db) => async (event, { fileLocation }) => { // abort if there is no manifest const hasManifest = await isFileAvailable(manifestPath); if (!hasManifest) { - mainWindow.webContents.send(LOADED_SPACE_CHANNEL, ERROR_ZIP_CORRUPTED); + mainWindow.webContents.send( + EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL, + ERROR_ZIP_CORRUPTED + ); return clean(extractPath); } const manifestString = await fsPromises.readFile(manifestPath); @@ -47,57 +54,104 @@ const loadSpace = (mainWindow, db) => async (event, { fileLocation }) => { // get handle to spaces collection const spaces = db.get(SPACES_COLLECTION); - const existingSpace = spaces.find({ id }).value(); - - // abort if there is already a space with that id - if (existingSpace) { - mainWindow.webContents.send( - LOADED_SPACE_CHANNEL, - ERROR_SPACE_ALREADY_AVAILABLE - ); - return clean(extractPath); - } - - // abort if there is no space - const hasSpace = await isFileAvailable(spacePath); - if (!hasSpace) { - mainWindow.webContents.send(LOADED_SPACE_CHANNEL, ERROR_ZIP_CORRUPTED); - return clean(extractPath); - } + const savedSpace = spaces.find({ id }).value(); const spaceString = await fsPromises.readFile(spacePath); const { - space, + space = {}, appInstanceResources: resources = [], actions = [], } = JSON.parse(spaceString); - const finalPath = `${VAR_FOLDER}/${id}`; + const elements = { space, resources, actions }; - // we need to wrap this operation to avoid errors in windows - performFileSystemOperation(fs.renameSync)(extractPath, finalPath); + return mainWindow.webContents.send(EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL, { + extractPath, + savedSpace, + elements, + }); + } catch (err) { + logger.error(err); + mainWindow.webContents.send( + EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL, + ERROR_GENERAL + ); + return clean(extractPath); + } +}; - const wasRenamed = await isFileAvailable(finalPath); +const cancelLoadSpace = mainWindow => async (event, { extractPath }) => { + const isCleanSuccessful = clean(extractPath); + mainWindow.webContents.send(CANCEL_LOAD_SPACE_CHANNEL, isCleanSuccessful); + return isCleanSuccessful; +}; - if (!wasRenamed) { - logger.error('unable to rename extract path'); - mainWindow.webContents.send(LOADED_SPACE_CHANNEL, ERROR_GENERAL); - return clean(extractPath); +const loadSpace = (mainWindow, db) => async ( + event, + { + extractPath, + elements: { space, actions, resources }, + selection: { + space: isSpaceSelected, + resources: isResourcesSelected, + actions: isActionsSelected, + }, + } +) => { + try { + // space must be always defined + if (_.isEmpty(space)) { + logger.debug('try to load undefined space'); + return mainWindow.webContents.send(LOADED_SPACE_CHANNEL, ERROR_GENERAL); } - // write space to database - spaces.push(space).write(); + // write space to database if selected + if (isSpaceSelected) { + const { id } = space; + const finalPath = `${VAR_FOLDER}/${id}`; + + // we need to wrap this operation to avoid errors in windows + performFileSystemOperation(fs.renameSync)(extractPath, finalPath); - // write resources to database - db.get(APP_INSTANCE_RESOURCES_COLLECTION) - .push(...resources) - .write(); + const wasRenamed = await isFileAvailable(finalPath); - // write actions to database - db.get(ACTIONS_COLLECTION) - .push(...actions) - .write(); + if (!wasRenamed) { + logger.error('unable to rename extract path'); + mainWindow.webContents.send(LOADED_SPACE_CHANNEL, ERROR_GENERAL); + return clean(extractPath); + } + + db.get(SPACES_COLLECTION) + .push(space) + .write(); + } else { + clean(extractPath); + } + + // write resources to database if selected + if (isResourcesSelected) { + if (_.isEmpty(resources)) { + logger.debug('try to load empty resources'); + return mainWindow.webContents.send(LOADED_SPACE_CHANNEL, ERROR_GENERAL); + } + db.get(APP_INSTANCE_RESOURCES_COLLECTION) + .push(...resources) + .write(); + } + + // write actions to database if selected + if (isActionsSelected) { + if (_.isEmpty(actions)) { + logger.debug('try to load empty actions'); + return mainWindow.webContents.send(LOADED_SPACE_CHANNEL, ERROR_GENERAL); + } + db.get(ACTIONS_COLLECTION) + .push(...actions) + .write(); + } - return mainWindow.webContents.send(LOADED_SPACE_CHANNEL, { spaceId: id }); + return mainWindow.webContents.send(LOADED_SPACE_CHANNEL, { + spaceId: space.id, + }); } catch (err) { logger.error(err); mainWindow.webContents.send(LOADED_SPACE_CHANNEL, ERROR_GENERAL); @@ -105,4 +159,4 @@ const loadSpace = (mainWindow, db) => async (event, { fileLocation }) => { } }; -module.exports = loadSpace; +module.exports = { cancelLoadSpace, extractFileToLoadSpace, loadSpace }; diff --git a/public/electron.js b/public/electron.js index 3a10a307..423f6b4e 100644 --- a/public/electron.js +++ b/public/electron.js @@ -58,6 +58,8 @@ const { SET_USER_MODE_CHANNEL, SET_SPACE_AS_FAVORITE_CHANNEL, SET_SPACE_AS_RECENT_CHANNEL, + EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL, + CANCEL_LOAD_SPACE_CHANNEL, } = require('./app/config/channels'); const env = require('./env.json'); const { @@ -94,6 +96,8 @@ const { getUserMode, setSpaceAsFavorite, setSpaceAsRecent, + cancelLoadSpace, + extractFileToLoadSpace, } = require('./app/listeners'); const isMac = require('./app/utils/isMac'); @@ -358,15 +362,24 @@ app.on('ready', async () => { // called when deleting a space ipcMain.on(DELETE_SPACE_CHANNEL, deleteSpace(mainWindow, db)); + // prompt when loading a space + ipcMain.on(SHOW_LOAD_SPACE_PROMPT_CHANNEL, showLoadSpacePrompt(mainWindow)); + // called when loading a space ipcMain.on(LOAD_SPACE_CHANNEL, loadSpace(mainWindow, db)); + // called when requesting to load a space + ipcMain.on( + EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL, + extractFileToLoadSpace(mainWindow, db) + ); + + // called when canceling to load a space + ipcMain.on(CANCEL_LOAD_SPACE_CHANNEL, cancelLoadSpace(mainWindow)); + // called when exporting a space ipcMain.on(EXPORT_SPACE_CHANNEL, exportSpace(mainWindow, db)); - // prompt when loading a space - ipcMain.on(SHOW_LOAD_SPACE_PROMPT_CHANNEL, showLoadSpacePrompt(mainWindow)); - // prompt when exporting a space ipcMain.on( SHOW_EXPORT_SPACE_PROMPT_CHANNEL, diff --git a/src/App.js b/src/App.js index 3532799e..f85b06fd 100644 --- a/src/App.js +++ b/src/App.js @@ -17,6 +17,7 @@ import LoadSpace from './components/LoadSpace'; import SpaceScreen from './components/space/SpaceScreen'; import SyncScreen from './components/space/SyncScreen'; import ExportSelectionScreen from './components/space/export/ExportSelectionScreen'; +import LoadSelectionScreen from './components/space/load/LoadSelectionScreen'; import DeveloperScreen from './components/developer/DeveloperScreen'; import { OnlineTheme, OfflineTheme } from './themes'; import Dashboard from './components/dashboard/Dashboard'; @@ -35,6 +36,7 @@ import { SIGN_IN_PATH, SAVED_SPACES_PATH, buildExportSelectionPathForSpaceId, + LOAD_SELECTION_SPACE_PATH, } from './config/paths'; import { getGeolocation, @@ -185,6 +187,11 @@ export class App extends Component { path={LOAD_SPACE_PATH} component={Authorization()(LoadSpace)} /> + dispatch => { + dispatch(flagLoadingSpace(true)); + window.ipcRenderer.send(LOAD_SPACE_CHANNEL, payload); + window.ipcRenderer.once(LOADED_SPACE_CHANNEL, (event, response) => { + switch (response) { + case ERROR_GENERAL: + toastr.error(ERROR_MESSAGE_HEADER, ERROR_LOADING_MESSAGE); + break; + default: + dispatch({ + type: LOAD_SPACE_SUCCEEDED, + }); + toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SPACE_LOADED_MESSAGE); + setSpaceAsRecent({ spaceId: response.spaceId, recent: true })(dispatch); + } + dispatch(flagLoadingSpace(false)); + }); +}; + +export const extractFileToLoadSpace = ({ fileLocation }) => dispatch => { + dispatch(flagExtractingFileToLoadSpace(true)); + window.ipcRenderer.send(EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL, { fileLocation }); + window.ipcRenderer.once( + EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL, + (event, response) => { + switch (response) { + case ERROR_ZIP_CORRUPTED: + toastr.error(ERROR_MESSAGE_HEADER, ERROR_ZIP_CORRUPTED_MESSAGE); + break; + case ERROR_JSON_CORRUPTED: + toastr.error(ERROR_MESSAGE_HEADER, ERROR_JSON_CORRUPTED_MESSAGE); + break; + case ERROR_SPACE_ALREADY_AVAILABLE: + toastr.error( + ERROR_MESSAGE_HEADER, + ERROR_SPACE_ALREADY_AVAILABLE_MESSAGE + ); + break; + case ERROR_GENERAL: + toastr.error(ERROR_MESSAGE_HEADER, ERROR_LOADING_MESSAGE); + break; + default: + dispatch({ + type: EXTRACT_FILE_TO_LOAD_SPACE_SUCCEEDED, + payload: response, + }); + } + dispatch(flagExtractingFileToLoadSpace(false)); + } + ); +}; + +export const cancelLoadSpace = payload => dispatch => { + dispatch(flagCancelingLoadSpace(true)); + window.ipcRenderer.send(CANCEL_LOAD_SPACE_CHANNEL, payload); + window.ipcRenderer.once(CANCEL_LOAD_SPACE_CHANNEL, (event, response) => { + switch (response) { + case ERROR_GENERAL: + toastr.error(ERROR_MESSAGE_HEADER, ERROR_LOADING_MESSAGE); + break; + default: + dispatch({ + type: CANCEL_LOAD_SPACE_SUCCEEDED, + }); + } + dispatch(flagCancelingLoadSpace(false)); + }); +}; diff --git a/src/actions/space.js b/src/actions/space.js index e2e267b7..b16ec406 100644 --- a/src/actions/space.js +++ b/src/actions/space.js @@ -3,7 +3,6 @@ import { GET_SPACES, FLAG_GETTING_SPACE, FLAG_GETTING_SPACES, - FLAG_LOADING_SPACE, CLEAR_SPACE, GET_SPACE_SUCCEEDED, FLAG_EXPORTING_SPACE, @@ -13,14 +12,10 @@ import { SAVE_SPACE_SUCCEEDED, FLAG_GETTING_SPACES_NEARBY, GET_SPACES_NEARBY_SUCCEEDED, - FLAG_SYNCING_SPACE, - SYNC_SPACE_SUCCEEDED, FLAG_CLEARING_USER_INPUT, SET_SPACE_SEARCH_QUERY_SUCCEEDED, } from '../types'; import { - ERROR_ZIP_CORRUPTED, - ERROR_JSON_CORRUPTED, ERROR_SPACE_ALREADY_AVAILABLE, ERROR_GENERAL, ERROR_DOWNLOADING_FILE, @@ -33,15 +28,11 @@ import { EXPORTED_SPACE_CHANNEL, GET_SPACE_CHANNEL, GET_SPACES_CHANNEL, - LOAD_SPACE_CHANNEL, - LOADED_SPACE_CHANNEL, SHOW_DELETE_SPACE_PROMPT_CHANNEL, RESPOND_DELETE_SPACE_PROMPT_CHANNEL, SHOW_EXPORT_SPACE_PROMPT_CHANNEL, RESPOND_EXPORT_SPACE_PROMPT_CHANNEL, SAVE_SPACE_CHANNEL, - SYNC_SPACE_CHANNEL, - SYNCED_SPACE_CHANNEL, CLEAR_USER_INPUT_CHANNEL, CLEARED_USER_INPUT_CHANNEL, RESPOND_CLEAR_USER_INPUT_PROMPT_CHANNEL, @@ -54,19 +45,13 @@ import { ERROR_EXPORTING_MESSAGE, ERROR_GETTING_SPACE_MESSAGE, ERROR_GETTING_SPACES_NEARBY, - ERROR_JSON_CORRUPTED_MESSAGE, - ERROR_LOADING_MESSAGE, ERROR_MESSAGE_HEADER, ERROR_SAVING_SPACE_MESSAGE, ERROR_SPACE_ALREADY_AVAILABLE_MESSAGE, - ERROR_ZIP_CORRUPTED_MESSAGE, SUCCESS_DELETING_MESSAGE, SUCCESS_EXPORTING_MESSAGE, SUCCESS_MESSAGE_HEADER, SUCCESS_SAVING_MESSAGE, - SUCCESS_SPACE_LOADED_MESSAGE, - SUCCESS_SYNCING_MESSAGE, - ERROR_SYNCING_MESSAGE, ERROR_CLEARING_USER_INPUT_MESSAGE, SUCCESS_CLEARING_USER_INPUT_MESSAGE, } from '../config/messages'; @@ -80,14 +65,11 @@ import { DEFAULT_RADIUS } from '../config/constants'; import { setSpaceAsFavorite, setSpaceAsRecent } from './user'; const flagGettingSpaces = createFlag(FLAG_GETTING_SPACES); -const flagLoadingSpace = createFlag(FLAG_LOADING_SPACE); const flagDeletingSpace = createFlag(FLAG_DELETING_SPACE); const flagExportingSpace = createFlag(FLAG_EXPORTING_SPACE); const flagSavingSpace = createFlag(FLAG_SAVING_SPACE); const flagGettingSpacesNearby = createFlag(FLAG_GETTING_SPACES_NEARBY); -const flagSyncingSpace = createFlag(FLAG_SYNCING_SPACE); const flagClearingUserInput = createFlag(FLAG_CLEARING_USER_INPUT); - /** * helper function to wrap a listener to the get space channel around a promise * @param online @@ -359,70 +341,6 @@ const clearUserInput = async ({ spaceId, userId }) => async dispatch => { } }; -const syncSpace = async ({ id }) => async dispatch => { - try { - const url = generateGetSpaceEndpoint(id); - const response = await fetch(url, DEFAULT_GET_REQUEST); - // throws if it is an error - await isErrorResponse(response); - const remoteSpace = await response.json(); - - dispatch(flagSyncingSpace(true)); - window.ipcRenderer.send(SYNC_SPACE_CHANNEL, { remoteSpace }); - - // this runs once the space has been synced - window.ipcRenderer.once(SYNCED_SPACE_CHANNEL, (event, res) => { - if (res === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE); - } else { - // update saved spaces in state - dispatch(getSpaces()); - - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SYNCING_MESSAGE); - dispatch({ - type: SYNC_SPACE_SUCCEEDED, - payload: res, - }); - } - dispatch(flagSyncingSpace(false)); - }); - } catch (err) { - dispatch(flagSyncingSpace(false)); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE); - } -}; - -const loadSpace = ({ fileLocation }) => dispatch => { - dispatch(flagLoadingSpace(true)); - window.ipcRenderer.send(LOAD_SPACE_CHANNEL, { fileLocation }); - window.ipcRenderer.once(LOADED_SPACE_CHANNEL, (event, response) => { - switch (response) { - case ERROR_ZIP_CORRUPTED: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_ZIP_CORRUPTED_MESSAGE); - break; - case ERROR_JSON_CORRUPTED: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_JSON_CORRUPTED_MESSAGE); - break; - case ERROR_SPACE_ALREADY_AVAILABLE: - toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_SPACE_ALREADY_AVAILABLE_MESSAGE - ); - break; - case ERROR_GENERAL: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_LOADING_MESSAGE); - break; - default: { - // add in recent spaces - setSpaceAsRecent({ recent: true, spaceId: response.spaceId })(dispatch); - - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SPACE_LOADED_MESSAGE); - } - } - dispatch(flagLoadingSpace(false)); - }); -}; - const getSpace = ({ id, saved = false, user }) => dispatch => { // only get the space from the api if not saved if (!saved) { @@ -467,7 +385,6 @@ const setSearchQuery = async payload => async dispatch => { }; export { - loadSpace, clearSpace, deleteSpace, exportSpace, @@ -477,7 +394,6 @@ export { getSpace, saveSpace, getSpacesNearby, - syncSpace, clearUserInput, createGetLocalSpace, createGetRemoteSpace, diff --git a/src/actions/syncSpace.js b/src/actions/syncSpace.js index 6207717e..2d78e014 100644 --- a/src/actions/syncSpace.js +++ b/src/actions/syncSpace.js @@ -1,4 +1,5 @@ -import { createGetLocalSpace, createGetRemoteSpace } from './space'; +import { toastr } from 'react-redux-toastr'; +import { createGetLocalSpace, createGetRemoteSpace, getSpaces } from './space'; import { GET_SYNC_REMOTE_SPACE_SUCCEEDED, GET_SYNC_LOCAL_SPACE_SUCCEEDED, @@ -7,7 +8,55 @@ import { CLEAR_SYNC_PHASES, FLAG_GETTING_SYNC_REMOTE_SPACE, FLAG_GETTING_SYNC_LOCAL_SPACE, + SYNC_SPACE_SUCCEEDED, + FLAG_SYNCING_SPACE, } from '../types'; +import { ERROR_GENERAL } from '../config/errors'; +import { SYNC_SPACE_CHANNEL, SYNCED_SPACE_CHANNEL } from '../config/channels'; +import { + SUCCESS_SYNCING_MESSAGE, + ERROR_SYNCING_MESSAGE, + SUCCESS_MESSAGE_HEADER, + ERROR_MESSAGE_HEADER, +} from '../config/messages'; +import { createFlag, isErrorResponse } from './common'; +import { generateGetSpaceEndpoint } from '../config/endpoints'; +import { DEFAULT_GET_REQUEST } from '../config/rest'; + +const flagSyncingSpace = createFlag(FLAG_SYNCING_SPACE); + +export const syncSpace = async ({ id }) => async dispatch => { + try { + const url = generateGetSpaceEndpoint(id); + const response = await fetch(url, DEFAULT_GET_REQUEST); + // throws if it is an error + await isErrorResponse(response); + const remoteSpace = await response.json(); + + dispatch(flagSyncingSpace(true)); + window.ipcRenderer.send(SYNC_SPACE_CHANNEL, { remoteSpace }); + + // this runs once the space has been synced + window.ipcRenderer.once(SYNCED_SPACE_CHANNEL, (event, res) => { + if (res === ERROR_GENERAL) { + toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE); + } else { + // update saved spaces in state + dispatch(getSpaces()); + + toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SYNCING_MESSAGE); + dispatch({ + type: SYNC_SPACE_SUCCEEDED, + payload: res, + }); + } + dispatch(flagSyncingSpace(false)); + }); + } catch (err) { + dispatch(flagSyncingSpace(false)); + toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE); + } +}; export const getRemoteSpaceForSync = payload => createGetRemoteSpace( diff --git a/src/components/LoadSpace.js b/src/components/LoadSpace.js index 1a1228c6..d8e305de 100644 --- a/src/components/LoadSpace.js +++ b/src/components/LoadSpace.js @@ -12,7 +12,6 @@ import Toolbar from '@material-ui/core/Toolbar'; import Button from '@material-ui/core/Button'; import FormControl from '@material-ui/core/FormControl'; import Input from '@material-ui/core/Input'; -import { loadSpace } from '../actions/space'; import './LoadSpace.css'; import Styles from '../Styles'; import Loader from './common/Loader'; @@ -27,6 +26,8 @@ import { LOAD_LOAD_BUTTON_ID, LOAD_INPUT_ID, } from '../config/selectors'; +import { LOAD_SELECTION_SPACE_PATH } from '../config/paths'; +import { extractFileToLoadSpace } from '../actions'; class LoadSpace extends Component { state = { @@ -35,12 +36,14 @@ class LoadSpace extends Component { static propTypes = { t: PropTypes.func.isRequired, - dispatchLoadSpace: PropTypes.func.isRequired, + dispatchExtractFileToLoadSpace: PropTypes.func.isRequired, theme: PropTypes.shape({ direction: PropTypes.string.isRequired }) .isRequired, activity: PropTypes.bool.isRequired, - history: PropTypes.shape({ length: PropTypes.number.isRequired }) - .isRequired, + history: PropTypes.shape({ + length: PropTypes.number.isRequired, + push: PropTypes.func.isRequired, + }).isRequired, classes: PropTypes.shape({ appBar: PropTypes.string.isRequired, root: PropTypes.string.isRequired, @@ -54,8 +57,20 @@ class LoadSpace extends Component { button: PropTypes.string.isRequired, formControl: PropTypes.string.isRequired, }).isRequired, + extractPath: PropTypes.string.isRequired, }; + componentDidUpdate() { + // load space from extractpath if it is set + const { + extractPath, + history: { push }, + } = this.props; + if (extractPath.length) { + push(LOAD_SELECTION_SPACE_PATH); + } + } + handleFileLocation = event => { const fileLocation = event.target ? event.target.value : event; this.setState({ fileLocation }); @@ -63,8 +78,8 @@ class LoadSpace extends Component { handleLoad = () => { const { fileLocation } = this.state; - const { dispatchLoadSpace } = this.props; - dispatchLoadSpace({ fileLocation }); + const { dispatchExtractFileToLoadSpace } = this.props; + dispatchExtractFileToLoadSpace({ fileLocation }); this.setState({ fileLocation }); }; @@ -145,11 +160,12 @@ class LoadSpace extends Component { } const mapDispatchToProps = { - dispatchLoadSpace: loadSpace, + dispatchExtractFileToLoadSpace: extractFileToLoadSpace, }; -const mapStateToProps = ({ Space }) => ({ - activity: Boolean(Space.getIn(['current', 'activity']).size), +const mapStateToProps = ({ loadSpace }) => ({ + extractPath: loadSpace.getIn(['extract', 'extractPath']), + activity: Boolean(loadSpace.getIn(['activity']).size), }); const ConnectedComponent = connect( diff --git a/src/components/space/export/ExportSelectionScreen.js b/src/components/space/export/ExportSelectionScreen.js index b4dfa41f..08c1c2a3 100644 --- a/src/components/space/export/ExportSelectionScreen.js +++ b/src/components/space/export/ExportSelectionScreen.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import AppBar from '@material-ui/core/AppBar/AppBar'; import Toolbar from '@material-ui/core/Toolbar/Toolbar'; +import Box from '@material-ui/core/Box'; import { withRouter } from 'react-router'; import clsx from 'clsx'; import { withTranslation } from 'react-i18next'; @@ -24,6 +25,20 @@ const styles = theme => ({ buttonGroup: { textAlign: 'center', }, + + spaceName: { + component: 'span', + fontStyle: 'italic', + + // following lines ensure long titles are shorten + display: 'inline-block', + maxWidth: 200, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + + marginLeft: 1, // simulate left space + }, }); class ExportSelectionScreen extends Component { @@ -64,7 +79,6 @@ class ExportSelectionScreen extends Component { }; state = { - space: true, actions: true, resources: true, }; @@ -90,11 +104,26 @@ class ExportSelectionScreen extends Component { }, dispatchExportSpace, } = this.props; - const { space, actions, resources } = this.state; - const selection = { space, actions, resources }; + const { actions, resources } = this.state; + + // always export space + const selection = { space: true, actions, resources }; dispatchExportSpace(id, name, userId, selection); }; + renderSpaceName = () => { + const { + theme, + location: { + state: { + space: { name }, + }, + }, + } = this.props; + // eslint-disable-next-line react/jsx-props-no-spreading + return {name}; + }; + render() { const { classes, @@ -103,7 +132,6 @@ class ExportSelectionScreen extends Component { activity, } = this.props; const { - space: isSpaceChecked, resources: isResourcesChecked, actions: isActionsChecked, } = this.state; @@ -126,15 +154,6 @@ class ExportSelectionScreen extends Component { ); } - const spaceCheckbox = ( - - ); - const resourcesCheckbox = (
- - {t('What do you want to export?')} + + {t('Export Space ')} + + + + {t('You are going to export')} + {this.renderSpaceName()} + + + {t('Include')} + {':'}
- ({ + ...Styles(theme), + buttonGroup: { + textAlign: 'center', + }, +}); + +class LoadSelectionScreen extends Component { + static propTypes = { + classes: PropTypes.shape({ + root: PropTypes.string.isRequired, + appBar: PropTypes.string.isRequired, + appBarShift: PropTypes.string.isRequired, + menuButton: PropTypes.string.isRequired, + hide: PropTypes.string.isRequired, + drawer: PropTypes.string.isRequired, + drawerPaper: PropTypes.string.isRequired, + drawerHeader: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + contentShift: PropTypes.string.isRequired, + buttonGroup: PropTypes.string.isRequired, + submitButton: PropTypes.string.isRequired, + button: PropTypes.string.isRequired, + }).isRequired, + theme: PropTypes.shape({ direction: PropTypes.string }).isRequired, + dispatchLoadSpace: PropTypes.func.isRequired, + dispatchCancelLoadSpace: PropTypes.func.isRequired, + activity: PropTypes.bool.isRequired, + history: PropTypes.shape({ + goBack: PropTypes.func.isRequired, + push: PropTypes.func.isRequired, + }).isRequired, + location: PropTypes.shape({ + search: PropTypes.string.isRequired, + state: PropTypes.shape({ + space: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }), + }), + }).isRequired, + t: PropTypes.func.isRequired, + elements: PropTypes.instanceOf(Map).isRequired, + extractPath: PropTypes.string.isRequired, + savedSpace: PropTypes.instanceOf(Map), + }; + + static defaultProps = { + savedSpace: Map(), + }; + + /* eslint-disable react/destructuring-assignment */ + state = { + // check space if it is not empty and is different from saved space + space: !this.props.elements.get('space').isEmpty(), + actions: !this.props.elements.get('actions').isEmpty(), + resources: !this.props.elements.get('resources').isEmpty(), + isSpaceDifferent: true, + }; + /* eslint-enable react/destructuring-assignment */ + + componentDidMount() { + const { savedSpace, elements } = this.props; + const space = elements.get('space'); + + // space is different if zip space is not empty and the space does not exist locally or + // there is a difference between currently saved space and space in zip + // it changes space checkbox as well + const isSpaceDifferent = + !space.isEmpty() && + (savedSpace.isEmpty() || + !isSpaceUpToDate(space.toJS(), savedSpace.toJS())); + + this.setState({ isSpaceDifferent, space: isSpaceDifferent }); + } + + componentWillUnmount() { + const { dispatchCancelLoadSpace, extractPath } = this.props; + dispatchCancelLoadSpace({ extractPath }); + } + + handleChange = event => { + this.setState({ [event.target.name]: event.target.checked }); + }; + + handleBack = () => { + const { + history: { goBack }, + extractPath, + dispatchCancelLoadSpace, + } = this.props; + goBack(); + dispatchCancelLoadSpace({ extractPath }); + }; + + handleSubmit = async () => { + const { + dispatchLoadSpace, + elements, + extractPath, + history: { push }, + } = this.props; + const { space, actions, resources } = this.state; + const selection = { space, actions, resources }; + await dispatchLoadSpace({ + extractPath, + elements: elements.toJS(), + selection, + }); + push(LOAD_SPACE_PATH); + }; + + renderCheckbox = (name, label, isChecked, disabled, emptyHelperText) => { + const checkbox = ( + + ); + + const { elements } = this.props; + const isEmpty = elements.get(name).isEmpty(); + + return ( + <> + + {isEmpty && {emptyHelperText}} + + ); + }; + + render() { + const { classes, t, activity, elements } = this.props; + const { + space: isSpaceChecked, + resources: isResourcesChecked, + actions: isActionsChecked, + isSpaceDifferent, + } = this.state; + + const selection = { isSpaceChecked, isResourcesChecked, isActionsChecked }; + + if (activity) { + return ( +
+ + + + +
+ +
+
+ ); + } + + return ( +
+
+ + {t('What do you want to load?')} + + +
+ + {this.renderCheckbox( + t('space'), + t('This Space'), + isSpaceChecked, + // space is always disabled: + // when the space does not exist (force load) + // when the space has change (force load) + // when the space has no change (no load) + true, + t(`This file does not contain a space`) + )} + {isSpaceDifferent ? ( + + {t(`The Space is different from local`)} + + ) : ( + {t('This space already exist')} + )} + + {this.renderCheckbox( + t('resources'), + t(`This Space's User Inputs`), + isResourcesChecked, + elements.get('resources').isEmpty(), + t(`This file does not contain any user input`) + )} + + {this.renderCheckbox( + t('actions'), + t(`This Space's analytics`), + isActionsChecked, + elements.get('actions').isEmpty(), + t(`This file does not contain any analytics`) + )} + +
+
+ + +
+
+
+ ); + } +} + +const mapStateToProps = ({ loadSpace: loadSpaceReducer }) => ({ + elements: loadSpaceReducer.getIn(['extract', 'elements']), + savedSpace: loadSpaceReducer.getIn(['extract', 'savedSpace']), + activity: Boolean(loadSpaceReducer.getIn(['activity']).size), + extractPath: loadSpaceReducer.getIn(['extract', 'extractPath']), +}); + +const mapDispatchToProps = { + dispatchLoadSpace: loadSpace, + dispatchCancelLoadSpace: cancelLoadSpace, +}; + +const TranslatedComponent = withTranslation()(LoadSelectionScreen); + +export default withRouter( + withStyles(styles, { withTheme: true })( + connect(mapStateToProps, mapDispatchToProps)(TranslatedComponent) + ) +); diff --git a/src/config/channels.js b/src/config/channels.js index f47ab1f0..fa56e3ad 100644 --- a/src/config/channels.js +++ b/src/config/channels.js @@ -8,14 +8,16 @@ module.exports = { GET_SPACES_CHANNEL: 'spaces:get', DELETE_SPACE_CHANNEL: 'space:delete', DELETED_SPACE_CHANNEL: 'space:deleted', + CANCEL_LOAD_SPACE_CHANNEL: 'space:load:cancel', + EXTRACT_FILE_TO_LOAD_SPACE_CHANNEL: 'space:load:extract-file', LOAD_SPACE_CHANNEL: 'space:load', LOADED_SPACE_CHANNEL: 'space:loaded', EXPORT_SPACE_CHANNEL: 'space:export', EXPORTED_SPACE_CHANNEL: 'space:exported', SHOW_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:show', SHOW_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:show', - RESPOND_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:response', RESPOND_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:respond', + RESPOND_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:respond', SHOW_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:show', RESPOND_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:respond', GET_USER_FOLDER_CHANNEL: 'user:folder:get', diff --git a/src/config/paths.js b/src/config/paths.js index ae174833..c4241f04 100644 --- a/src/config/paths.js +++ b/src/config/paths.js @@ -2,6 +2,7 @@ export const HOME_PATH = '/'; export const SPACES_NEARBY_PATH = '/nearby'; export const VISIT_PATH = '/visit-space'; export const LOAD_SPACE_PATH = '/load-space'; +export const LOAD_SELECTION_SPACE_PATH = `/load-space/selection`; export const SETTINGS_PATH = '/settings'; export const SPACE_PATH = '/space/:id'; export const DEVELOPER_PATH = '/developer'; diff --git a/src/reducers/SpaceReducer.js b/src/reducers/SpaceReducer.js index 1697166d..f8865f0d 100644 --- a/src/reducers/SpaceReducer.js +++ b/src/reducers/SpaceReducer.js @@ -7,7 +7,6 @@ import { FLAG_GETTING_SPACE, FLAG_DELETING_SPACE, FLAG_GETTING_SPACES, - FLAG_LOADING_SPACE, FLAG_EXPORTING_SPACE, DELETE_SPACE_SUCCESS, SAVE_SPACE_SUCCEEDED, @@ -59,7 +58,6 @@ export default (state = INITIAL_STATE, { type, payload }) => { case FLAG_SAVING_SPACE: case FLAG_GETTING_SPACE: case FLAG_GETTING_SPACES: - case FLAG_LOADING_SPACE: case FLAG_EXPORTING_SPACE: case FLAG_DELETING_SPACE: case FLAG_SYNCING_SPACE: diff --git a/src/reducers/index.js b/src/reducers/index.js index 4b045523..c8d687cf 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -6,6 +6,7 @@ import Developer from './DeveloperReducer'; import layout from './layout'; import authentication from './authenticationReducer'; import syncSpace from './syncSpaceReducer'; +import loadSpace from './loadSpaceReducer'; export default combineReducers({ // todo: keys should always be camelCase @@ -16,4 +17,5 @@ export default combineReducers({ toastr, authentication, syncSpace, + loadSpace, }); diff --git a/src/reducers/loadSpaceReducer.js b/src/reducers/loadSpaceReducer.js new file mode 100644 index 00000000..27906896 --- /dev/null +++ b/src/reducers/loadSpaceReducer.js @@ -0,0 +1,44 @@ +import { Map, List, fromJS } from 'immutable'; +import { + FLAG_EXTRACTING_FILE_TO_LOAD_SPACE, + FLAG_CANCELING_TO_LOAD_SPACE, + CANCEL_LOAD_SPACE_SUCCEEDED, + FLAG_LOADING_SPACE, + LOAD_SPACE_SUCCEEDED, + EXTRACT_FILE_TO_LOAD_SPACE_SUCCEEDED, +} from '../types'; +import { updateActivityList } from './common'; + +const DEFAULT_EXTRACT_PARAMETERS = Map({ + extractPath: '', + savedSpace: Map(), + spaceId: null, // ? + elements: Map({ + space: Map(), + actions: List(), + resources: List(), + }), +}); + +const INITIAL_STATE = Map({ + activity: List(), + extract: DEFAULT_EXTRACT_PARAMETERS, +}); + +export default (state = INITIAL_STATE, { type, payload }) => { + switch (type) { + case FLAG_LOADING_SPACE: + case FLAG_EXTRACTING_FILE_TO_LOAD_SPACE: + case FLAG_CANCELING_TO_LOAD_SPACE: + return state.updateIn(['activity'], updateActivityList(payload)); + case EXTRACT_FILE_TO_LOAD_SPACE_SUCCEEDED: { + return state.setIn(['extract'], fromJS(payload)); + } + // clear extract info on cancel or on successful load + case LOAD_SPACE_SUCCEEDED: + case CANCEL_LOAD_SPACE_SUCCEEDED: + return state.setIn(['extract'], DEFAULT_EXTRACT_PARAMETERS); + default: + return state; + } +}; diff --git a/src/types/index.js b/src/types/index.js index 9ac446cc..591d6e02 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -8,3 +8,4 @@ export * from './layout'; export * from './action'; export * from './authentication'; export * from './syncSpace'; +export * from './loadSpace'; diff --git a/src/types/loadSpace.js b/src/types/loadSpace.js new file mode 100644 index 00000000..249aeb69 --- /dev/null +++ b/src/types/loadSpace.js @@ -0,0 +1,9 @@ +export const LOAD_SPACE_SUCCEEDED = 'LOAD_SPACE_SUCCEEDED'; +export const FLAG_EXTRACTING_FILE_TO_LOAD_SPACE = + 'FLAG_EXTRACTING_FILE_TO_LOAD_SPACE'; +export const FLAG_CANCELING_TO_LOAD_SPACE = 'FLAG_CANCELING_TO_LOAD_SPACE'; +export const CANCEL_LOAD_SPACE = 'CANCEL_LOAD_SPACE'; +export const CANCEL_LOAD_SPACE_SUCCEEDED = 'CANCEL_LOAD_SPACE_SUCCEEDED'; +export const FLAG_LOADING_SPACE = 'FLAG_LOADING_SPACE'; +export const EXTRACT_FILE_TO_LOAD_SPACE_SUCCEEDED = + 'EXTRACT_FILE_TO_LOAD_SPACE_SUCCEEDED'; diff --git a/src/types/space.js b/src/types/space.js index 737b234a..3897b5f9 100644 --- a/src/types/space.js +++ b/src/types/space.js @@ -1,6 +1,5 @@ export const GET_SPACES = 'GET_SPACES'; export const FLAG_GETTING_SPACE = 'FLAG_GETTING_SPACE'; -export const FLAG_LOADING_SPACE = 'FLAG_LOADING_SPACE'; export const FLAG_GETTING_SPACES = 'FLAG_GETTING_SPACES'; export const FLAG_DELETING_SPACE = 'FLAG_DELETING_SPACE'; export const FLAG_EXPORTING_SPACE = 'FLAG_EXPORTING_SPACES';