* 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 HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
|
||||||
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
||||||
export const SET_ASSETS = 'SET_ASSETS';
|
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 axios from 'axios';
|
||||||
import objectID from 'bson-objectid';
|
import objectID from 'bson-objectid';
|
||||||
import each from 'async/each';
|
import each from 'async/each';
|
||||||
import { isEqual, pick } from 'lodash';
|
import isEqual from 'lodash/isEqual';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { showToast, setToastText } from './toast';
|
import { showToast, setToastText } from './toast';
|
||||||
import {
|
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) {
|
export function getProject(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(justOpenedProject());
|
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) {
|
export function saveProject(selectedFile = null, autosave = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = 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) {
|
if (state.user.id && state.project.owner && state.project.owner.id !== state.user.id) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
const formParams = Object.assign({}, state.project);
|
const formParams = Object.assign({}, state.project);
|
||||||
formParams.files = [...state.files];
|
formParams.files = [...state.files];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
console.log('selected file being updated');
|
|
||||||
const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id);
|
const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id);
|
||||||
fileToUpdate.content = selectedFile.content;
|
fileToUpdate.content = selectedFile.content;
|
||||||
}
|
}
|
||||||
if (state.project.id) {
|
if (state.project.id) {
|
||||||
return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
|
return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const currentState = getState();
|
dispatch(endSavingProject());
|
||||||
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(setUnsavedChanges(false));
|
dispatch(setUnsavedChanges(false));
|
||||||
|
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
|
||||||
|
if (hasChanges) {
|
||||||
|
dispatch(setUnsavedChanges(true));
|
||||||
}
|
}
|
||||||
dispatch(setProject(savedProject));
|
dispatch(setProject(synchedProject));
|
||||||
dispatch({
|
dispatch(projectSaveSuccess());
|
||||||
type: ActionTypes.PROJECT_SAVE_SUCCESS
|
|
||||||
});
|
|
||||||
if (!autosave) {
|
if (!autosave) {
|
||||||
if (state.ide.justOpenedProject && state.preferences.autosave) {
|
if (state.ide.justOpenedProject && state.preferences.autosave) {
|
||||||
dispatch(showToast(5500));
|
dispatch(showToast(5500));
|
||||||
|
@ -110,30 +160,32 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
|
dispatch(endSavingProject());
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
} else if (response.status === 409) {
|
} else if (response.status === 409) {
|
||||||
dispatch(showErrorModal('staleProject'));
|
dispatch(showErrorModal('staleProject'));
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch(projectSaveFail(response.data));
|
||||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
|
||||||
error: response.data
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
dispatch(endSavingProject());
|
||||||
|
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
|
||||||
|
if (hasChanges) {
|
||||||
|
dispatch(setNewProject(synchedProject));
|
||||||
dispatch(setUnsavedChanges(false));
|
dispatch(setUnsavedChanges(false));
|
||||||
dispatch(setProject(response.data));
|
|
||||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
||||||
dispatch({
|
dispatch(setUnsavedChanges(true));
|
||||||
type: ActionTypes.NEW_PROJECT,
|
} else {
|
||||||
project: response.data,
|
dispatch(setNewProject(synchedProject));
|
||||||
owner: response.data.user,
|
dispatch(setUnsavedChanges(false));
|
||||||
files: response.data.files
|
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
||||||
});
|
}
|
||||||
|
dispatch(projectSaveSuccess());
|
||||||
if (!autosave) {
|
if (!autosave) {
|
||||||
if (state.preferences.autosave) {
|
if (state.preferences.autosave) {
|
||||||
dispatch(showToast(5500));
|
dispatch(showToast(5500));
|
||||||
|
@ -147,13 +199,11 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
|
dispatch(endSavingProject());
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch(projectSaveFail(response.data));
|
||||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
|
||||||
error: response.data
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -166,22 +216,28 @@ export function autosaveProject() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProject() {
|
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 })
|
axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
dispatch(endSavingProject());
|
||||||
dispatch({
|
|
||||||
type: ActionTypes.NEW_PROJECT,
|
|
||||||
project: response.data,
|
|
||||||
owner: response.data.user,
|
|
||||||
files: response.data.files
|
|
||||||
});
|
|
||||||
dispatch(setUnsavedChanges(false));
|
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({
|
.catch((response) => {
|
||||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
dispatch(endSavingProject());
|
||||||
error: response.data
|
dispatch(projectSaveFail(response.data));
|
||||||
}));
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,12 +307,7 @@ export function cloneProject() {
|
||||||
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
||||||
dispatch({
|
dispatch(setNewProject(response.data));
|
||||||
type: ActionTypes.NEW_PROJECT,
|
|
||||||
project: response.data,
|
|
||||||
owner: response.data.user,
|
|
||||||
files: response.data.files
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch(response => dispatch({
|
||||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
type: ActionTypes.PROJECT_SAVE_FAIL,
|
||||||
|
|
|
@ -26,7 +26,7 @@ class ErrorModal extends React.Component {
|
||||||
staleProject() {
|
staleProject() {
|
||||||
return (
|
return (
|
||||||
<p>
|
<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>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,8 @@ const initialState = () => {
|
||||||
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
|
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
|
||||||
return {
|
return {
|
||||||
name: generatedName,
|
name: generatedName,
|
||||||
updatedAt: ''
|
updatedAt: '',
|
||||||
|
isSaving: false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,14 +29,16 @@ const project = (state, action) => {
|
||||||
id: action.project.id,
|
id: action.project.id,
|
||||||
name: action.project.name,
|
name: action.project.name,
|
||||||
updatedAt: action.project.updatedAt,
|
updatedAt: action.project.updatedAt,
|
||||||
owner: action.owner
|
owner: action.owner,
|
||||||
|
isSaving: false
|
||||||
};
|
};
|
||||||
case ActionTypes.SET_PROJECT:
|
case ActionTypes.SET_PROJECT:
|
||||||
return {
|
return {
|
||||||
id: action.project.id,
|
id: action.project.id,
|
||||||
name: action.project.name,
|
name: action.project.name,
|
||||||
updatedAt: action.project.updatedAt,
|
updatedAt: action.project.updatedAt,
|
||||||
owner: action.owner
|
owner: action.owner,
|
||||||
|
isSaving: false
|
||||||
};
|
};
|
||||||
case ActionTypes.RESET_PROJECT:
|
case ActionTypes.RESET_PROJECT:
|
||||||
return initialState();
|
return initialState();
|
||||||
|
@ -45,6 +48,10 @@ const project = (state, action) => {
|
||||||
return Object.assign({}, state, { isEditingName: false });
|
return Object.assign({}, state, { isEditingName: false });
|
||||||
case ActionTypes.SET_PROJECT_SAVED_TIME:
|
case ActionTypes.SET_PROJECT_SAVED_TIME:
|
||||||
return Object.assign({}, state, { updatedAt: action.value });
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import format from 'date-fns/format';
|
||||||
import isUrl from 'is-url';
|
import isUrl from 'is-url';
|
||||||
import jsdom, { serializeDocument } from 'jsdom';
|
import jsdom, { serializeDocument } from 'jsdom';
|
||||||
import isBefore from 'date-fns/is_before';
|
import isBefore from 'date-fns/is_before';
|
||||||
|
import isAfter from 'date-fns/is_after';
|
||||||
import request from 'request';
|
import request from 'request';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
import Project from '../models/project';
|
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.' });
|
res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) {
|
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.' });
|
res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
Project.findByIdAndUpdate(
|
Project.findByIdAndUpdate(
|
||||||
req.params.project_id,
|
req.params.project_id,
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue