* 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
This commit is contained in:
parent
94eb6f1ac9
commit
7d1901649f
5 changed files with 120 additions and 58 deletions
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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());
|
||||
|
@ -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(setUnsavedChanges(true));
|
||||
} else {
|
||||
dispatch(endSavingProject());
|
||||
dispatch(setUnsavedChanges(false));
|
||||
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
|
||||
if (hasChanges) {
|
||||
dispatch(setUnsavedChanges(true));
|
||||
}
|
||||
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));
|
||||
|
@ -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(endSavingProject());
|
||||
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
|
||||
if (hasChanges) {
|
||||
dispatch(setNewProject(synchedProject));
|
||||
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(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));
|
||||
|
@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,8 @@ const initialState = () => {
|
|||
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
|
||||
return {
|
||||
name: generatedName,
|
||||
updatedAt: ''
|
||||
updatedAt: '',
|
||||
isSaving: false
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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,
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue