Skip to content

Project synching, for #790 #1039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,6 @@ export const HIDE_HELP_MODAL = 'HIDE_HELP_MODAL';
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
export const SET_ASSETS = 'SET_ASSETS';

export const START_SAVING_PROJECT = 'START_SAVING_PROJECT';
export const END_SAVING_PROJECT = 'END_SAVING_PROJECT';
151 changes: 101 additions & 50 deletions client/modules/IDE/actions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { browserHistory } from 'react-router';
import axios from 'axios';
import objectID from 'bson-objectid';
import each from 'async/each';
import { isEqual, pick } from 'lodash';
import isEqual from 'lodash/isEqual';
import * as ActionTypes from '../../../constants';
import { showToast, setToastText } from './toast';
import {
Expand Down Expand Up @@ -32,6 +32,22 @@ export function setProjectName(name) {
};
}

export function projectSaveFail(error) {
return {
type: ActionTypes.PROJECT_SAVE_FAIL,
error
};
}

export function setNewProject(project) {
return {
type: ActionTypes.NEW_PROJECT,
project,
owner: project.user,
files: project.files
};
}

export function getProject(id) {
return (dispatch, getState) => {
dispatch(justOpenedProject());
Expand Down Expand Up @@ -66,37 +82,71 @@ export function clearPersistedState() {
};
}

export function startSavingProject() {
return {
type: ActionTypes.START_SAVING_PROJECT
};
}

export function endSavingProject() {
return {
type: ActionTypes.END_SAVING_PROJECT
};
}

export function projectSaveSuccess() {
return {
type: ActionTypes.PROJECT_SAVE_SUCCESS
};
}

// want a function that will check for changes on the front end
function getSynchedProject(currentState, responseProject) {
let hasChanges = false;
const synchedProject = Object.assign({}, responseProject);
const currentFiles = currentState.files.map(({ name, children, content }) => ({ name, children, content }));
const responseFiles = responseProject.files.map(({ name, children, content }) => ({ name, children, content }));
if (!isEqual(currentFiles, responseFiles)) {
synchedProject.files = currentState.files;
hasChanges = true;
}
if (currentState.project.name !== responseProject.name) {
synchedProject.name = currentState.project.name;
hasChanges = true;
}
return {
synchedProject,
hasChanges
};
}

export function saveProject(selectedFile = null, autosave = false) {
return (dispatch, getState) => {
const state = getState();
if (state.project.isSaving) {
return Promise.resolve();
}
dispatch(startSavingProject());
if (state.user.id && state.project.owner && state.project.owner.id !== state.user.id) {
return Promise.reject();
}
const formParams = Object.assign({}, state.project);
formParams.files = [...state.files];
if (selectedFile) {
console.log('selected file being updated');
const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id);
fileToUpdate.content = selectedFile.content;
}
if (state.project.id) {
return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
.then((response) => {
const currentState = getState();
const savedProject = Object.assign({}, response.data);
if (!isEqual(
pick(currentState.files, ['name', 'children', 'content']),
pick(response.data.files, ['name', 'children', 'content'])
)) {
savedProject.files = currentState.files;
dispatch(endSavingProject());
dispatch(setUnsavedChanges(false));
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
if (hasChanges) {
dispatch(setUnsavedChanges(true));
} else {
dispatch(setUnsavedChanges(false));
}
dispatch(setProject(savedProject));
dispatch({
type: ActionTypes.PROJECT_SAVE_SUCCESS
});
dispatch(setProject(synchedProject));
dispatch(projectSaveSuccess());
if (!autosave) {
if (state.ide.justOpenedProject && state.preferences.autosave) {
dispatch(showToast(5500));
Expand All @@ -110,30 +160,32 @@ export function saveProject(selectedFile = null, autosave = false) {
}
})
.catch((response) => {
dispatch(endSavingProject());
if (response.status === 403) {
dispatch(showErrorModal('staleSession'));
} else if (response.status === 409) {
dispatch(showErrorModal('staleProject'));
} else {
dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL,
error: response.data
});
dispatch(projectSaveFail(response.data));
}
});
}

return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then((response) => {
dispatch(setUnsavedChanges(false));
dispatch(setProject(response.data));
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,
project: response.data,
owner: response.data.user,
files: response.data.files
});
dispatch(endSavingProject());
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
if (hasChanges) {
dispatch(setNewProject(synchedProject));
dispatch(setUnsavedChanges(false));
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch(setUnsavedChanges(true));
} else {
dispatch(setNewProject(synchedProject));
dispatch(setUnsavedChanges(false));
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
}
dispatch(projectSaveSuccess());
if (!autosave) {
if (state.preferences.autosave) {
dispatch(showToast(5500));
Expand All @@ -147,13 +199,11 @@ export function saveProject(selectedFile = null, autosave = false) {
}
})
.catch((response) => {
dispatch(endSavingProject());
if (response.status === 403) {
dispatch(showErrorModal('staleSession'));
} else {
dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL,
error: response.data
});
dispatch(projectSaveFail(response.data));
}
});
};
Expand All @@ -166,22 +216,28 @@ export function autosaveProject() {
}

export function createProject() {
return (dispatch) => {
return (dispatch, getState) => {
const state = getState();
if (state.project.isSaving) {
Promise.resolve();
return;
}
dispatch(startSavingProject());
axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true })
.then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,
project: response.data,
owner: response.data.user,
files: response.data.files
});
dispatch(endSavingProject());
dispatch(setUnsavedChanges(false));
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
if (hasChanges) {
dispatch(setUnsavedChanges(true));
}
dispatch(setNewProject(synchedProject));
})
.catch(response => dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL,
error: response.data
}));
.catch((response) => {
dispatch(endSavingProject());
dispatch(projectSaveFail(response.data));
});
};
}

Expand Down Expand Up @@ -251,12 +307,7 @@ export function cloneProject() {
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,
project: response.data,
owner: response.data.user,
files: response.data.files
});
dispatch(setNewProject(response.data));
})
.catch(response => dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL,
Expand Down
2 changes: 1 addition & 1 deletion client/modules/IDE/components/ErrorModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ErrorModal extends React.Component {
staleProject() {
return (
<p>
The project you have attempted to save is out of date. Please refresh the page.
The project you have attempted to save has been saved from another window. Please refresh the page to see the latest version.
</p>
);
}
Expand Down
13 changes: 10 additions & 3 deletions client/modules/IDE/reducers/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const initialState = () => {
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
return {
name: generatedName,
updatedAt: ''
updatedAt: '',
isSaving: false
};
};

Expand All @@ -28,14 +29,16 @@ const project = (state, action) => {
id: action.project.id,
name: action.project.name,
updatedAt: action.project.updatedAt,
owner: action.owner
owner: action.owner,
isSaving: false
};
case ActionTypes.SET_PROJECT:
return {
id: action.project.id,
name: action.project.name,
updatedAt: action.project.updatedAt,
owner: action.owner
owner: action.owner,
isSaving: false
};
case ActionTypes.RESET_PROJECT:
return initialState();
Expand All @@ -45,6 +48,10 @@ const project = (state, action) => {
return Object.assign({}, state, { isEditingName: false });
case ActionTypes.SET_PROJECT_SAVED_TIME:
return Object.assign({}, state, { updatedAt: action.value });
case ActionTypes.START_SAVING_PROJECT:
return Object.assign({}, state, { isSaving: true });
case ActionTypes.START_STOP_PROJECT:
return Object.assign({}, state, { isSaving: false });
default:
return state;
}
Expand Down
9 changes: 5 additions & 4 deletions server/controllers/project.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import format from 'date-fns/format';
import isUrl from 'is-url';
import jsdom, { serializeDocument } from 'jsdom';
import isBefore from 'date-fns/is_before';
import isAfter from 'date-fns/is_after';
import request from 'request';
import slugify from 'slugify';
import Project from '../models/project';
Expand Down Expand Up @@ -43,10 +44,10 @@ export function updateProject(req, res) {
res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
return;
}
// if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) {
// res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
// return;
// }
if (req.body.updatedAt && isAfter(new Date(project.updatedAt), req.body.updatedAt)) {
res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
return;
}
Project.findByIdAndUpdate(
req.params.project_id,
{
Expand Down