Skip to content

Commit 7d19016

Browse files
authored
Project synching, for #790 (#1039)
* add isSaving to project reducer, move actions to functions, start work to get comprehensive frontend/backend syncing working * handle making changes while saving project, handle saving from another window * add change to handle saving new sketch, and adding new changes while saving
1 parent 94eb6f1 commit 7d19016

File tree

5 files changed

+120
-58
lines changed

5 files changed

+120
-58
lines changed

client/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,6 @@ export const HIDE_HELP_MODAL = 'HIDE_HELP_MODAL';
117117
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
118118
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
119119
export const SET_ASSETS = 'SET_ASSETS';
120+
121+
export const START_SAVING_PROJECT = 'START_SAVING_PROJECT';
122+
export const END_SAVING_PROJECT = 'END_SAVING_PROJECT';

client/modules/IDE/actions/project.js

Lines changed: 101 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { browserHistory } from 'react-router';
22
import axios from 'axios';
33
import objectID from 'bson-objectid';
44
import each from 'async/each';
5-
import { isEqual, pick } from 'lodash';
5+
import isEqual from 'lodash/isEqual';
66
import * as ActionTypes from '../../../constants';
77
import { showToast, setToastText } from './toast';
88
import {
@@ -32,6 +32,22 @@ export function setProjectName(name) {
3232
};
3333
}
3434

35+
export function projectSaveFail(error) {
36+
return {
37+
type: ActionTypes.PROJECT_SAVE_FAIL,
38+
error
39+
};
40+
}
41+
42+
export function setNewProject(project) {
43+
return {
44+
type: ActionTypes.NEW_PROJECT,
45+
project,
46+
owner: project.user,
47+
files: project.files
48+
};
49+
}
50+
3551
export function getProject(id) {
3652
return (dispatch, getState) => {
3753
dispatch(justOpenedProject());
@@ -66,37 +82,71 @@ export function clearPersistedState() {
6682
};
6783
}
6884

85+
export function startSavingProject() {
86+
return {
87+
type: ActionTypes.START_SAVING_PROJECT
88+
};
89+
}
90+
91+
export function endSavingProject() {
92+
return {
93+
type: ActionTypes.END_SAVING_PROJECT
94+
};
95+
}
96+
97+
export function projectSaveSuccess() {
98+
return {
99+
type: ActionTypes.PROJECT_SAVE_SUCCESS
100+
};
101+
}
102+
103+
// want a function that will check for changes on the front end
104+
function getSynchedProject(currentState, responseProject) {
105+
let hasChanges = false;
106+
const synchedProject = Object.assign({}, responseProject);
107+
const currentFiles = currentState.files.map(({ name, children, content }) => ({ name, children, content }));
108+
const responseFiles = responseProject.files.map(({ name, children, content }) => ({ name, children, content }));
109+
if (!isEqual(currentFiles, responseFiles)) {
110+
synchedProject.files = currentState.files;
111+
hasChanges = true;
112+
}
113+
if (currentState.project.name !== responseProject.name) {
114+
synchedProject.name = currentState.project.name;
115+
hasChanges = true;
116+
}
117+
return {
118+
synchedProject,
119+
hasChanges
120+
};
121+
}
122+
69123
export function saveProject(selectedFile = null, autosave = false) {
70124
return (dispatch, getState) => {
71125
const state = getState();
126+
if (state.project.isSaving) {
127+
return Promise.resolve();
128+
}
129+
dispatch(startSavingProject());
72130
if (state.user.id && state.project.owner && state.project.owner.id !== state.user.id) {
73131
return Promise.reject();
74132
}
75133
const formParams = Object.assign({}, state.project);
76134
formParams.files = [...state.files];
77135
if (selectedFile) {
78-
console.log('selected file being updated');
79136
const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id);
80137
fileToUpdate.content = selectedFile.content;
81138
}
82139
if (state.project.id) {
83140
return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
84141
.then((response) => {
85-
const currentState = getState();
86-
const savedProject = Object.assign({}, response.data);
87-
if (!isEqual(
88-
pick(currentState.files, ['name', 'children', 'content']),
89-
pick(response.data.files, ['name', 'children', 'content'])
90-
)) {
91-
savedProject.files = currentState.files;
142+
dispatch(endSavingProject());
143+
dispatch(setUnsavedChanges(false));
144+
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
145+
if (hasChanges) {
92146
dispatch(setUnsavedChanges(true));
93-
} else {
94-
dispatch(setUnsavedChanges(false));
95147
}
96-
dispatch(setProject(savedProject));
97-
dispatch({
98-
type: ActionTypes.PROJECT_SAVE_SUCCESS
99-
});
148+
dispatch(setProject(synchedProject));
149+
dispatch(projectSaveSuccess());
100150
if (!autosave) {
101151
if (state.ide.justOpenedProject && state.preferences.autosave) {
102152
dispatch(showToast(5500));
@@ -110,30 +160,32 @@ export function saveProject(selectedFile = null, autosave = false) {
110160
}
111161
})
112162
.catch((response) => {
163+
dispatch(endSavingProject());
113164
if (response.status === 403) {
114165
dispatch(showErrorModal('staleSession'));
115166
} else if (response.status === 409) {
116167
dispatch(showErrorModal('staleProject'));
117168
} else {
118-
dispatch({
119-
type: ActionTypes.PROJECT_SAVE_FAIL,
120-
error: response.data
121-
});
169+
dispatch(projectSaveFail(response.data));
122170
}
123171
});
124172
}
125173

126174
return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
127175
.then((response) => {
128-
dispatch(setUnsavedChanges(false));
129-
dispatch(setProject(response.data));
130-
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
131-
dispatch({
132-
type: ActionTypes.NEW_PROJECT,
133-
project: response.data,
134-
owner: response.data.user,
135-
files: response.data.files
136-
});
176+
dispatch(endSavingProject());
177+
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
178+
if (hasChanges) {
179+
dispatch(setNewProject(synchedProject));
180+
dispatch(setUnsavedChanges(false));
181+
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
182+
dispatch(setUnsavedChanges(true));
183+
} else {
184+
dispatch(setNewProject(synchedProject));
185+
dispatch(setUnsavedChanges(false));
186+
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
187+
}
188+
dispatch(projectSaveSuccess());
137189
if (!autosave) {
138190
if (state.preferences.autosave) {
139191
dispatch(showToast(5500));
@@ -147,13 +199,11 @@ export function saveProject(selectedFile = null, autosave = false) {
147199
}
148200
})
149201
.catch((response) => {
202+
dispatch(endSavingProject());
150203
if (response.status === 403) {
151204
dispatch(showErrorModal('staleSession'));
152205
} else {
153-
dispatch({
154-
type: ActionTypes.PROJECT_SAVE_FAIL,
155-
error: response.data
156-
});
206+
dispatch(projectSaveFail(response.data));
157207
}
158208
});
159209
};
@@ -166,22 +216,28 @@ export function autosaveProject() {
166216
}
167217

168218
export function createProject() {
169-
return (dispatch) => {
219+
return (dispatch, getState) => {
220+
const state = getState();
221+
if (state.project.isSaving) {
222+
Promise.resolve();
223+
return;
224+
}
225+
dispatch(startSavingProject());
170226
axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true })
171227
.then((response) => {
172-
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
173-
dispatch({
174-
type: ActionTypes.NEW_PROJECT,
175-
project: response.data,
176-
owner: response.data.user,
177-
files: response.data.files
178-
});
228+
dispatch(endSavingProject());
179229
dispatch(setUnsavedChanges(false));
230+
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
231+
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
232+
if (hasChanges) {
233+
dispatch(setUnsavedChanges(true));
234+
}
235+
dispatch(setNewProject(synchedProject));
180236
})
181-
.catch(response => dispatch({
182-
type: ActionTypes.PROJECT_SAVE_FAIL,
183-
error: response.data
184-
}));
237+
.catch((response) => {
238+
dispatch(endSavingProject());
239+
dispatch(projectSaveFail(response.data));
240+
});
185241
};
186242
}
187243

@@ -251,12 +307,7 @@ export function cloneProject() {
251307
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
252308
.then((response) => {
253309
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
254-
dispatch({
255-
type: ActionTypes.NEW_PROJECT,
256-
project: response.data,
257-
owner: response.data.user,
258-
files: response.data.files
259-
});
310+
dispatch(setNewProject(response.data));
260311
})
261312
.catch(response => dispatch({
262313
type: ActionTypes.PROJECT_SAVE_FAIL,

client/modules/IDE/components/ErrorModal.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class ErrorModal extends React.Component {
2626
staleProject() {
2727
return (
2828
<p>
29-
The project you have attempted to save is out of date. Please refresh the page.
29+
The project you have attempted to save has been saved from another window. Please refresh the page to see the latest version.
3030
</p>
3131
);
3232
}

client/modules/IDE/reducers/project.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const initialState = () => {
1212
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
1313
return {
1414
name: generatedName,
15-
updatedAt: ''
15+
updatedAt: '',
16+
isSaving: false
1617
};
1718
};
1819

@@ -28,14 +29,16 @@ const project = (state, action) => {
2829
id: action.project.id,
2930
name: action.project.name,
3031
updatedAt: action.project.updatedAt,
31-
owner: action.owner
32+
owner: action.owner,
33+
isSaving: false
3234
};
3335
case ActionTypes.SET_PROJECT:
3436
return {
3537
id: action.project.id,
3638
name: action.project.name,
3739
updatedAt: action.project.updatedAt,
38-
owner: action.owner
40+
owner: action.owner,
41+
isSaving: false
3942
};
4043
case ActionTypes.RESET_PROJECT:
4144
return initialState();
@@ -45,6 +48,10 @@ const project = (state, action) => {
4548
return Object.assign({}, state, { isEditingName: false });
4649
case ActionTypes.SET_PROJECT_SAVED_TIME:
4750
return Object.assign({}, state, { updatedAt: action.value });
51+
case ActionTypes.START_SAVING_PROJECT:
52+
return Object.assign({}, state, { isSaving: true });
53+
case ActionTypes.START_STOP_PROJECT:
54+
return Object.assign({}, state, { isSaving: false });
4855
default:
4956
return state;
5057
}

server/controllers/project.controller.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import format from 'date-fns/format';
33
import isUrl from 'is-url';
44
import jsdom, { serializeDocument } from 'jsdom';
55
import isBefore from 'date-fns/is_before';
6+
import isAfter from 'date-fns/is_after';
67
import request from 'request';
78
import slugify from 'slugify';
89
import Project from '../models/project';
@@ -43,10 +44,10 @@ export function updateProject(req, res) {
4344
res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
4445
return;
4546
}
46-
// if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) {
47-
// res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
48-
// return;
49-
// }
47+
if (req.body.updatedAt && isAfter(new Date(project.updatedAt), req.body.updatedAt)) {
48+
res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
49+
return;
50+
}
5051
Project.findByIdAndUpdate(
5152
req.params.project_id,
5253
{

0 commit comments

Comments
 (0)