diff --git a/src/components/Account/Login/Login.actions.js b/src/components/Account/Login/Login.actions.js index cf96125bf..31022cc73 100644 --- a/src/components/Account/Login/Login.actions.js +++ b/src/components/Account/Login/Login.actions.js @@ -147,7 +147,10 @@ export function login({ email, password, activatedData }, type = 'local') { ); if (loginData.communicators && loginData.communicators.length) { - currentCommunicator = loginData.communicators[0]; + const lastRemoteSavedCommunicatorIndex = + loginData.communicators.length - 1; + currentCommunicator = + loginData.communicators[lastRemoteSavedCommunicatorIndex]; //use the latest communicator } const localBoardsIds = []; diff --git a/src/components/Communicator/Communicator.actions.js b/src/components/Communicator/Communicator.actions.js index 221d30c6d..54f5d1b3f 100644 --- a/src/components/Communicator/Communicator.actions.js +++ b/src/components/Communicator/Communicator.actions.js @@ -17,11 +17,15 @@ import { UPDATE_API_COMMUNICATOR_STARTED, GET_API_MY_COMMUNICATORS_SUCCESS, GET_API_MY_COMMUNICATORS_FAILURE, - GET_API_MY_COMMUNICATORS_STARTED + GET_API_MY_COMMUNICATORS_STARTED, + SYNC_COMMUNICATORS } from './Communicator.constants'; import { defaultCommunicatorID } from './Communicator.reducer'; import API from '../../api'; import shortid from 'shortid'; +import moment from 'moment'; +import { switchBoard } from '../Board/Board.actions'; +import history from './../../history'; export function importCommunicator(communicator) { return { @@ -230,17 +234,24 @@ export function verifyAndUpsertCommunicator( */ export function getApiMyCommunicators() { - return dispatch => { + return async dispatch => { dispatch(getApiMyCommunicatorsStarted()); - return API.getCommunicators() - .then(res => { - dispatch(getApiMyCommunicatorsSuccess(res)); - return res; - }) - .catch(err => { - dispatch(getApiMyCommunicatorsFailure(err.message)); - throw new Error(err.message); - }); + try { + const res = await API.getCommunicators(); + dispatch(getApiMyCommunicatorsSuccess(res)); + if (res?.data && res.data.length) { + try { + await dispatch(syncCommunicators(res.data)); + } catch (e) { + console.error(e); + } + } + + return res; + } catch (err) { + dispatch(getApiMyCommunicatorsFailure(err.message)); + throw new Error(err.message); + } }; } @@ -291,3 +302,85 @@ export function updateDefaultBoardsIncluded(boardAlreadyIncludedData) { defaultBoardsIncluded: boardAlreadyIncludedData }; } + +export function syncCommunicators(remoteCommunicators) { + const reconcileCommunicators = (local, remote) => { + if (local.lastEdited && remote.lastEdited) { + if (moment(local.lastEdited).isAfter(remote.lastEdited)) { + return local; + } + if (moment(local.lastEdited).isBefore(remote.lastEdited)) { + return remote; + } + if (moment(local.lastEdited).isSame(remote.lastEdited)) { + return remote; + } + } + return remote; + }; + const getActiveCommunicator = getState => { + return getState().communicator.communicators.find( + c => c.id === getState().communicator.activeCommunicatorId + ); + }; + + return async (dispatch, getState) => { + const localCommunicators = getState().communicator.communicators; + const updatedCommunicators = [...localCommunicators]; + + for (const remote of remoteCommunicators) { + const localIndex = localCommunicators.findIndex( + local => local.id === remote.id + ); + + if (localIndex !== -1) { + // If the communicator exists locally, reconcile the two + const reconciled = reconcileCommunicators( + localCommunicators[localIndex], + remote + ); + if (reconciled === localCommunicators[localIndex]) { + // Local is more recent, update the server + try { + const res = await dispatch( + updateApiCommunicator(localCommunicators[localIndex]) + ); + updatedCommunicators[localIndex] = res; + } catch (e) { + console.error(e); + } + } else { + updatedCommunicators[localIndex] = reconciled; + } + } else { + // If the communicator does not exist locally, add it + updatedCommunicators.push(remote); + } + } + + const activeCommunicatorId = getActiveCommunicator(getState).id ?? null; + const lastRemoteSavedCommunicatorId = remoteCommunicators[0].id ?? null; //The last communicator saved on the server + const needToChangeActiveCommunicator = + activeCommunicatorId !== lastRemoteSavedCommunicatorId && + updatedCommunicators.length && + lastRemoteSavedCommunicatorId && + updatedCommunicators.findIndex( + communicator => communicator.id === lastRemoteSavedCommunicatorId + ) !== -1; + + dispatch({ + type: SYNC_COMMUNICATORS, + communicators: updatedCommunicators, + activeCommunicatorId: needToChangeActiveCommunicator + ? lastRemoteSavedCommunicatorId + : activeCommunicatorId + }); + + if (needToChangeActiveCommunicator) { + const newActiveCommunicator = getActiveCommunicator(getState); + const rootBoard = newActiveCommunicator.rootBoard; + dispatch(switchBoard(rootBoard)); + history.replace(rootBoard); + } + }; +} diff --git a/src/components/Communicator/Communicator.constants.js b/src/components/Communicator/Communicator.constants.js index 3caa635b6..6b681e9a2 100644 --- a/src/components/Communicator/Communicator.constants.js +++ b/src/components/Communicator/Communicator.constants.js @@ -31,3 +31,4 @@ export const GET_API_MY_COMMUNICATORS_FAILURE = 'cboard/Communicator/GET_API_MY_COMMUNICATORS_FAILURE'; export const GET_API_MY_COMMUNICATORS_STARTED = 'cboard/Communicator/GET_API_MY_COMMUNICATORS_STARTED'; +export const SYNC_COMMUNICATORS = 'Communicator/SYNC_COMMUNICATORS'; diff --git a/src/components/Communicator/Communicator.reducer.js b/src/components/Communicator/Communicator.reducer.js index cadca19ef..5b19818b2 100644 --- a/src/components/Communicator/Communicator.reducer.js +++ b/src/components/Communicator/Communicator.reducer.js @@ -19,9 +19,11 @@ import { UPDATE_API_COMMUNICATOR_STARTED, GET_API_MY_COMMUNICATORS_SUCCESS, GET_API_MY_COMMUNICATORS_FAILURE, - GET_API_MY_COMMUNICATORS_STARTED + GET_API_MY_COMMUNICATORS_STARTED, + SYNC_COMMUNICATORS } from './Communicator.constants'; import { LOGIN_SUCCESS, LOGOUT } from '../Account/Login/Login.constants'; +import moment from 'moment'; export const defaultCommunicatorID = 'cboard_default'; const initialState = { @@ -55,9 +57,13 @@ function communicatorReducer(state = initialState, action) { }; case CREATE_COMMUNICATOR: + const newCommunicator = { + ...action.payload, + lastEdited: moment().format() + }; return { ...state, - communicators: state.communicators.concat(action.payload) + communicators: state.communicators.concat(newCommunicator) }; case EDIT_COMMUNICATOR: @@ -67,7 +73,11 @@ function communicatorReducer(state = initialState, action) { let newState = { ...state }; if (communicatorIndex >= 0) { - newState.communicators[communicatorIndex] = action.payload; + const updatedCommunicator = { + ...action.payload, + lastEdited: moment().format() + }; + newState.communicators[communicatorIndex] = updatedCommunicator; } return newState; @@ -96,6 +106,7 @@ function communicatorReducer(state = initialState, action) { if (index !== -1) { const updatedCommunicators = [...state.communicators]; updatedCommunicators[index].boards.push(action.boardId); + updatedCommunicators[index].lastEdited = moment().format(); return { ...state, communicators: updatedCommunicators @@ -112,6 +123,7 @@ function communicatorReducer(state = initialState, action) { const bindex = activeCommunicator.boards.indexOf(action.boardId); if (bindex !== -1) { dupdatedCommunicators[index].boards.splice(bindex, 1); + dupdatedCommunicators[index].lastEdited = moment().format(); return { ...state, communicators: dupdatedCommunicators @@ -135,6 +147,7 @@ function communicatorReducer(state = initialState, action) { 1, action.nextBoardId ); + updatedCommunicators[index].lastEdited = moment().format(); return { ...state, communicators: updatedCommunicators @@ -163,6 +176,7 @@ function communicatorReducer(state = initialState, action) { updatedCommunicators[ index ].defaultBoardsIncluded = defaultBoardsIncluded; + updatedCommunicators[index].lastEdited = moment().format(); return { ...state, @@ -179,6 +193,7 @@ function communicatorReducer(state = initialState, action) { const updatedCommunicators = [...state.communicators]; updatedCommunicators[index].defaultBoardsIncluded = action.defaultBoardsIncluded; + updatedCommunicators[index].lastEdited = moment().format(); return { ...state, @@ -199,7 +214,11 @@ function communicatorReducer(state = initialState, action) { : state.activeCommunicatorId, communicators: state.communicators.map(communicator => communicator.id === action.communicatorId - ? { ...communicator, id: action.communicator.id } + ? { + ...communicator, + id: action.communicator.id, + lastEdited: action.communicator.lastEdited + } : communicator ) }; @@ -216,7 +235,15 @@ function communicatorReducer(state = initialState, action) { case UPDATE_API_COMMUNICATOR_SUCCESS: return { ...state, - isFetching: false + isFetching: false, + communicators: state.communicators.map(communicator => + communicator.id === action.communicator.id + ? { + ...communicator, + lastEdited: action.communicator.lastEdited + } + : communicator + ) }; case UPDATE_API_COMMUNICATOR_FAILURE: return { @@ -229,25 +256,9 @@ function communicatorReducer(state = initialState, action) { isFetching: true }; case GET_API_MY_COMMUNICATORS_SUCCESS: - let flag = false; - const myCommunicators = [...state.communicators]; - for (let i = 0; i < action.communicators.data.length; i++) { - for (let j = 0; j < myCommunicators.length; j++) { - if (myCommunicators[j].id === action.communicators.data[i].id) { - myCommunicators[j].boards = action.communicators.data[i].boards; - flag = true; - break; - } - } - if (!flag) { - myCommunicators.push(action.communicators.data[i]); - flag = false; - } - } return { ...state, - isFetching: false, - communicators: myCommunicators + isFetching: false }; case GET_API_MY_COMMUNICATORS_FAILURE: return { @@ -259,6 +270,12 @@ function communicatorReducer(state = initialState, action) { ...state, isFetching: true }; + case SYNC_COMMUNICATORS: + return { + ...state, + communicators: action.communicators, + activeCommunicatorId: action.activeCommunicatorId + }; default: return state; } diff --git a/src/components/Communicator/__tests__/Communicator.reducer.test.js b/src/components/Communicator/__tests__/Communicator.reducer.test.js index 1b6703f41..1cee3ea57 100644 --- a/src/components/Communicator/__tests__/Communicator.reducer.test.js +++ b/src/components/Communicator/__tests__/Communicator.reducer.test.js @@ -21,6 +21,7 @@ import { GET_API_MY_COMMUNICATORS_STARTED } from '../Communicator.constants'; import { LOGIN_SUCCESS, LOGOUT } from '../../Account/Login/Login.constants'; +import moment from 'moment'; let mockComm, defaultCommunicatorID, initialState; describe('reducer', () => { @@ -32,7 +33,8 @@ describe('reducer', () => { email: 'anything@cboard.io', id: '123', name: "Cboard's Communicator", - rootBoard: '1' + rootBoard: '1', + lastEdited: moment().format() }; defaultCommunicatorID = 'cboard_default'; initialState = { @@ -124,7 +126,8 @@ describe('reducer', () => { }); it('should handle updateApiCommunicatorSuccess', () => { const updateApiCommunicatorSuccess = { - type: UPDATE_API_COMMUNICATOR_SUCCESS + type: UPDATE_API_COMMUNICATOR_SUCCESS, + communicator: initialState }; expect( communicatorReducer(initialState, updateApiCommunicatorSuccess)