save: save dialog and sagas

This commit is contained in:
muerwre 2018-11-27 14:48:57 +07:00
parent b586663827
commit 8fcca6587e
10 changed files with 161 additions and 88 deletions

View file

@ -11,7 +11,6 @@ import { CancelDialog } from '$components/save/CancelDialog';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
setMode, setMode,
setLogo, setLogo,
@ -24,12 +23,13 @@ import {
clearCancel, clearCancel,
stopEditing, stopEditing,
setEditing, setEditing,
sendSaveRequest,
} from '$redux/user/actions'; } from '$redux/user/actions';
type Props = { type Props = {
mode: String, mode: String,
activeSticker: String, activeSticker: String,
windth: Number, width: Number,
} }
export const Component = (props: Props) => { export const Component = (props: Props) => {
@ -83,6 +83,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({
stopEditing, stopEditing,
setEditing, setEditing,
setMode, setMode,
sendSaveRequest,
}, dispatch); }, dispatch);
export const EditorDialog = connect( export const EditorDialog = connect(

View file

@ -3,86 +3,63 @@ import { getUrlData, pushPath } from '$utils/history';
import { toTranslit } from '$utils/format'; import { toTranslit } from '$utils/format';
import { TIPS } from '$constants/tips'; import { TIPS } from '$constants/tips';
import { MODES } from '$constants/modes'; import { MODES } from '$constants/modes';
import { postMap } from '$utils/api';
import classnames from 'classnames'; type Props = {
address: String, // initial?
title: String, // initial?
export class SaveDialog extends React.Component { save_error: String,
save_finished: Boolean,
save_overwriting: Boolean,
save_processing: Boolean,
setMode: Function,
sendSaveRequest: Function,
};
type State = {
address: String,
title: String,
};
export class SaveDialog extends React.Component<Props, State> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
address: props.address || '', address: props.address || '',
title: props.title || '', title: props.title || '',
error: '',
sending: false,
finished: false,
overwriting: false,
}; };
} }
getAddress = () => { getAddress = () => {
const { path } = getUrlData(); const { path } = getUrlData();
const { title, address } = this.state; const { title, address } = this.state;
return toTranslit(address.trim()) || toTranslit(title.trim()) || toTranslit(path.trim());
return toTranslit(address.trim()) || toTranslit(title.trim().toLowerCase()) || toTranslit(path.trim());
}; };
setTitle = ({ target: { value } }) => this.setState({ title: (value || '') }); setTitle = ({ target: { value } }) => this.setState({ title: (value || '') });
setAddress = ({ target: { value } }) => this.setState({ address: (value || '') }); setAddress = ({ target: { value } }) => this.setState({ address: (value || '') });
cancelSaving = () => this.props.editor.changeMode(MODES.NONE); // cancelSaving = () => this.props.editor.changeMode(MODES.NONE);
cancelSaving = () => this.props.setMode(MODES.NONE);
sendSaveRequest = (e, force = false) => { sendSaveRequest = (e, force = false) => {
const { route, stickers } = this.props.editor.dumpData();
const { title } = this.state; const { title } = this.state;
const { id, token } = this.props.user; const address = this.getAddress();
postMap({ this.props.sendSaveRequest({
id, title, address, force,
token, });
route,
stickers,
title,
force,
address: this.getAddress(),
}).then(this.parseResponse).catch(console.warn);
}; };
forceSaveRequest = e => this.sendSaveRequest(e, true); forceSaveRequest = e => this.sendSaveRequest(e, true);
parseResponse = data => {
if (data.success) return this.setSuccess(data);
if (data.mode === 'overwriting') return this.setOverwrite(data.description);
return this.setError(data.description);
};
setSuccess = ({ address, description }) => {
pushPath(`/${address}/edit`);
console.log('addr?', address);
this.props.editor.setAddress(address);
this.props.editor.owner = this.props.user.id;
this.props.editor.setInitialData();
this.setState({
error: description, finished: true, sending: true, overwriting: false
});
};
setOverwrite = error => this.setState({
error, finished: false, sending: true, overwriting: true
});
setError = error => this.setState({
error, finished: false, sending: true, overwriting: false
});
render() { render() {
const { const { title } = this.state;
title, error, finished, overwriting, sending const { save_error, save_finished, save_overwriting, save_processing } = this.props;
} = this.state;
const { host } = getUrlData(); const { host } = getUrlData();
return ( return (
@ -90,41 +67,39 @@ export class SaveDialog extends React.Component {
<div className="save-title"> <div className="save-title">
<div className="save-title-input"> <div className="save-title-input">
<label className="save-title-label">Название</label> <label className="save-title-label">Название</label>
<input type="text" value={title} onChange={this.setTitle} autoFocus /> <input type="text" value={title} onChange={this.setTitle} autoFocus readOnly={save_finished} />
</div> </div>
</div> </div>
<div className="save-description"> <div className="save-description">
<div className="save-address-input"> <div className="save-address-input">
<label className="save-address-label">http://{host}/</label> <label className="save-address-label">http://{host}/</label>
<input type="text" value={this.getAddress().substr(0, 32)} onChange={this.setAddress} /> <input type="text" value={this.getAddress().substr(0, 32)} onChange={this.setAddress} readOnly={save_finished} />
</div> </div>
<div className="save-text"> <div className="save-text">
{ { save_error || TIPS.SAVE_INFO }
error || TIPS.SAVE_INFO
}
</div> </div>
<div className="save-buttons"> <div className="save-buttons">
<div className="save-buttons-text" /> <div className="save-buttons-text" />
<div className={classnames({ 'button-group': !finished })}> <div>
{ !finished && { !save_finished &&
<div className="button" onClick={this.cancelSaving}>Отмена</div> <div className="button" onClick={this.cancelSaving}>Отмена</div>
} }
{ {
(!sending || (sending && !overwriting && !finished)) && !save_finished && !save_overwriting &&
<div className="button primary" onClick={this.sendSaveRequest}>Сохранить</div> <div className="button primary" onClick={this.sendSaveRequest}>Сохранить</div>
} }
{ {
sending && overwriting && save_overwriting &&
<div className="button danger" onClick={this.forceSaveRequest}>Перезаписать</div> <div className="button danger" onClick={this.forceSaveRequest}>Перезаписать</div>
} }
{ finished && { save_finished &&
<div className="button success" onClick={this.cancelSaving}>Отлично, спасибо!</div> <div className="button success" onClick={this.cancelSaving}>Отлично, спасибо!</div>
} }

View file

@ -1,3 +1,6 @@
export const TIPS = { export const TIPS = {
SAVE_INFO: 'Вы можете задать своё название маршрута и адрес, по которому он будет доступен.' SAVE_INFO: 'Никто, кроме вас не сможет изменить маршрут - только создать его копию и сохранить по другому адресу',
SAVE_TIMED_OUT: 'Сервер не ответил на запрос, попробуйте позже',
SAVE_EMPTY: 'Этот маршрут пуст, нарисуйте что-нибудь для начала',
SAVE_SUCCESS: 'Маршрут сохранен - он будет доступен по указанному адресу'
}; };

View file

@ -95,20 +95,23 @@ export class Editor {
getEditing = () => store.getState().user.editing; getEditing = () => store.getState().user.editing;
getChanged = () => store.getState().user.changed; getChanged = () => store.getState().user.changed;
getRouterPoints = () => store.getState().user.routerPoints; getRouterPoints = () => store.getState().user.routerPoints;
getDistance = () => store.getState().user.distance;
setMode = value => store.dispatch(setMode(value)); setMode = value => store.dispatch(setMode(value));
setDistance = value => store.dispatch(setDistance(value));
setChanged = value => store.dispatch(setChanged(value)); setChanged = value => store.dispatch(setChanged(value));
setRouterPoints = value => store.dispatch(setRouterPoints(value)); setRouterPoints = value => store.dispatch(setRouterPoints(value));
setActiveSticker = value => store.dispatch(setActiveSticker(value)); setActiveSticker = value => store.dispatch(setActiveSticker(value));
setTitle = value => store.dispatch(setTitle(value)); setTitle = value => store.dispatch(setTitle(value));
setAddress = value => store.dispatch(setAddress(value)); setAddress = value => store.dispatch(setAddress(value));
setDistance = value => {
if (this.getDistance() !== value) store.dispatch(setDistance(value));
};
clearMode = () => this.setMode(MODES.NONE); clearMode = () => this.setMode(MODES.NONE);
clearChanged = () => store.dispatch(setChanged(false)); clearChanged = () => store.dispatch(setChanged(false));
startPoly = () => { startPoly = () => {
console.log(this.getRouterPoints());
if (this.getRouterPoints()) this.router.clearAll(); if (this.getRouterPoints()) this.router.clearAll();
this.poly.continue(); this.poly.continue();
@ -121,16 +124,15 @@ export class Editor {
}; };
createStickerOnClick = (e) => { createStickerOnClick = (e) => {
// todo: move to sagas?
if (!e || !e.latlng || !this.activeSticker) return; if (!e || !e.latlng || !this.activeSticker) return;
const { latlng } = e; const { latlng } = e;
this.stickers.createSticker({ latlng, sticker: this.activeSticker }); this.stickers.createSticker({ latlng, sticker: this.activeSticker });
this.setActiveSticker(null); this.setActiveSticker(null);
this.setChanged(true);
}; };
changeMode = mode => { changeMode = mode => {
// todo: check if TOGGLING works (we changing MODE from the sagas now)
if (this.mode === mode) { if (this.mode === mode) {
if (this.switches[mode] && this.switches[mode].toggle) { if (this.switches[mode] && this.switches[mode].toggle) {
// if we have special function on mode when it clicked again // if we have special function on mode when it clicked again
@ -202,14 +204,9 @@ export class Editor {
}; };
clearAll = () => { clearAll = () => {
// todo: move to sagas
this.poly.clearAll(); this.poly.clearAll();
this.router.clearAll(); this.router.clearAll();
this.stickers.clearAll(); this.stickers.clearAll();
// this.setActiveSticker(null);
// this.setMode(MODES.NONE);
// this.clearChanged();
}; };
@ -308,11 +305,17 @@ export class Editor {
// return (route.length > 1 && stickers.length > 0); // return (route.length > 1 && stickers.length > 0);
// }; // };
isEmpty = () => {
const { route, stickers } = this.dumpData();
return (!route || route.length < 1) && (!stickers || stickers.length <= 0);
};
hasEmptyHistory = () => { hasEmptyHistory = () => {
const { route, stickers } = this.initialData; const { route, stickers } = this.initialData;
return (!route || route.length < 1) && (!stickers || stickers.length <= 0); return (!route || route.length < 1) && (!stickers || stickers.length <= 0);
} };
} }
export const editor = new Editor({}); export const editor = new Editor({});

View file

@ -1,7 +1,7 @@
import { ACTIONS } from '$redux/user/constants'; import { ACTIONS } from '$redux/user/constants';
export const setUser = user => ({ type: ACTIONS.SET_USER, user }); export const setUser = user => ({ type: ACTIONS.SET_USER, user });
export const userLogout = user => ({ type: ACTIONS.USER_LOGOUT }); export const userLogout = () => ({ type: ACTIONS.USER_LOGOUT });
export const setEditing = editing => ({ type: ACTIONS.SET_EDITING, editing }); export const setEditing = editing => ({ type: ACTIONS.SET_EDITING, editing });
@ -24,3 +24,11 @@ export const clearPoly = () => ({ type: ACTIONS.CLEAR_POLY });
export const clearStickers = () => ({ type: ACTIONS.CLEAR_STICKERS }); export const clearStickers = () => ({ type: ACTIONS.CLEAR_STICKERS });
export const clearAll = () => ({ type: ACTIONS.CLEAR_ALL }); export const clearAll = () => ({ type: ACTIONS.CLEAR_ALL });
export const clearCancel = () => ({ type: ACTIONS.CLEAR_CANCEL }); export const clearCancel = () => ({ type: ACTIONS.CLEAR_CANCEL });
export const sendSaveRequest = payload => ({ type: ACTIONS.SEND_SAVE_REQUEST, ...payload });
export const cancelSaveRequest = () => ({ type: ACTIONS.CANCEL_SAVE_REQUEST });
export const setSaveSuccess = payload => ({ type: ACTIONS.SET_SAVE_SUCCESS, ...payload });
export const setSaveError = save_error => ({ type: ACTIONS.SET_SAVE_ERROR, save_error });
export const setSaveOverwrite = () => ({ type: ACTIONS.SET_SAVE_OVERWRITE });

View file

@ -22,4 +22,11 @@ export const ACTIONS = {
CLEAR_STICKERS: 'CLEAR_STICKERS', CLEAR_STICKERS: 'CLEAR_STICKERS',
CLEAR_ALL: 'CLEAR_ALL', CLEAR_ALL: 'CLEAR_ALL',
CLEAR_CANCEL: 'CLEAR_CANCEL', CLEAR_CANCEL: 'CLEAR_CANCEL',
SEND_SAVE_REQUEST: 'SEND_SAVE_REQUEST',
CANCEL_SAVE_REQUEST: 'CANCEL_SAVE_REQUEST',
SET_SAVE_SUCCESS: 'SET_SAVE_SUCCESS',
SET_SAVE_ERROR: 'SET_SAVE_ERROR',
SET_SAVE_OVERWRITE: 'SET_SAVE_OVERWRITE',
}; };

View file

@ -18,7 +18,9 @@ const setUser = (state, { user }) => ({
}); });
const setEditing = (state, { editing }) => ({ ...state, editing }); const setEditing = (state, { editing }) => ({ ...state, editing });
const setChanged = (state, { changed }) => ({ ...state, changed }); const setChanged = (state, { changed }) => ({
...state, changed, ...state, save_overwriting: false, save_finished: false, save_processing: false, save_error: '',
});
const setMode = (state, { mode }) => ({ ...state, mode }); const setMode = (state, { mode }) => ({ ...state, mode });
const setDistance = (state, { distance }) => ({ const setDistance = (state, { distance }) => ({
...state, ...state,
@ -33,6 +35,17 @@ const setLogo = (state, { logo }) => ({ ...state, logo });
const setTitle = (state, { title }) => ({ ...state, title }); const setTitle = (state, { title }) => ({ ...state, title });
const setAddress = (state, { address }) => ({ ...state, address }); const setAddress = (state, { address }) => ({ ...state, address });
const sendSaveRequest = state => ({ ...state, save_processing: true, });
const setSaveError = (state, { save_error }) => ({
...state, save_error, save_finished: false, save_processing: false
});
const setSaveOverwrite = state => ({
...state, save_overwriting: true, save_finished: false, save_processing: false
});
const setSaveSuccess = (state, { save_error }) => ({
...state, save_overwriting: false, save_finished: true, save_processing: false, save_error
});
const HANDLERS = { const HANDLERS = {
[ACTIONS.SET_USER]: setUser, [ACTIONS.SET_USER]: setUser,
[ACTIONS.SET_EDITING]: setEditing, [ACTIONS.SET_EDITING]: setEditing,
@ -44,6 +57,11 @@ const HANDLERS = {
[ACTIONS.SET_LOGO]: setLogo, [ACTIONS.SET_LOGO]: setLogo,
[ACTIONS.SET_TITLE]: setTitle, [ACTIONS.SET_TITLE]: setTitle,
[ACTIONS.SET_ADDRESS]: setAddress, [ACTIONS.SET_ADDRESS]: setAddress,
[ACTIONS.SET_SAVE_ERROR]: setSaveError,
[ACTIONS.SET_SAVE_OVERWRITE]: setSaveOverwrite,
[ACTIONS.SET_SAVE_SUCCESS]: setSaveSuccess,
[ACTIONS.SEND_SAVE_REQUEST]: sendSaveRequest,
}; };
export const INITIAL_STATE = { export const INITIAL_STATE = {
@ -58,6 +76,11 @@ export const INITIAL_STATE = {
title: 0, title: 0,
address: '', address: '',
changed: false, changed: false,
save_error: '',
save_finished: false,
save_overwriting: false,
save_processing: false,
}; };
export const userReducer = createReducer(INITIAL_STATE, HANDLERS); export const userReducer = createReducer(INITIAL_STATE, HANDLERS);

View file

@ -1,12 +1,22 @@
import { REHYDRATE } from 'redux-persist'; import { REHYDRATE } from 'redux-persist';
import { takeLatest, select, call, put, takeEvery } from 'redux-saga/effects'; import { delay } from 'redux-saga';
import { checkUserToken, getGuestToken, getStoredMap } from '$utils/api'; import { takeLatest, select, call, put, takeEvery, race, take } from 'redux-saga/effects';
import { setActiveSticker, setChanged, setEditing, setMode, setUser } from '$redux/user/actions'; import { checkUserToken, getGuestToken, getStoredMap, postMap } from '$utils/api';
import {
setActiveSticker, setAddress,
setChanged,
setEditing,
setMode,
setSaveError,
setSaveOverwrite, setSaveSuccess, setTitle,
setUser
} from '$redux/user/actions';
import { getUrlData, pushPath } from '$utils/history'; import { getUrlData, pushPath } from '$utils/history';
import { editor } from '$modules/Editor'; import { editor } from '$modules/Editor';
import { ACTIONS } from '$redux/user/constants'; import { ACTIONS } from '$redux/user/constants';
import { MODES } from '$constants/modes'; import { MODES } from '$constants/modes';
import { DEFAULT_USER } from '$constants/auth'; import { DEFAULT_USER } from '$constants/auth';
import { TIPS } from '$constants/tips';
const getUser = state => (state.user.user); const getUser = state => (state.user.user);
const getState = state => (state.user); const getState = state => (state.user);
@ -172,6 +182,41 @@ function* clearSaga({ type }) {
yield put(setMode(MODES.NONE)); yield put(setMode(MODES.NONE));
} }
function* sendSaveRequestSaga({ title, address, force }) {
if (editor.isEmpty()) {
return yield put(setSaveError(TIPS.SAVE_EMPTY));
}
const { route, stickers } = editor.dumpData();
const { id, token } = yield select(getUser);
const { result, timeout, cancel } = yield race({
result: postMap({
id, token, route, stickers, title, force, address
}),
timeout: delay(10000),
cancel: take(ACTIONS.CANCEL_SAVE_REQUEST),
});
if (cancel) return yield put(setMode(MODES.NONE));
if (result && result.mode === 'overwriting') return yield put(setSaveOverwrite());
if (timeout || !result || !result.success || !result.address) return yield put(setSaveError(TIPS.SAVE_TIMED_OUT));
return yield put(setSaveSuccess({ address: result.address, save_error: TIPS.SAVE_SUCCESS, title }));
}
function* setSaveSuccessSaga({ address, title }) {
const { id } = yield select(getUser);
pushPath(`/${address}/edit`);
yield put(setTitle(title));
yield put(setAddress(address));
// yield editor.setAddress(address);
yield editor.owner = id;
return yield editor.setInitialData();
}
export function* userSaga() { export function* userSaga() {
// ASYNCHRONOUS!!! :-) // ASYNCHRONOUS!!! :-)
@ -194,4 +239,6 @@ export function* userSaga() {
ACTIONS.CLEAR_CANCEL, ACTIONS.CLEAR_CANCEL,
], clearSaga); ], clearSaga);
yield takeLatest(ACTIONS.SEND_SAVE_REQUEST, sendSaveRequestSaga);
yield takeLatest(ACTIONS.SET_SAVE_SUCCESS, setSaveSuccessSaga);
} }

View file

@ -1,5 +1,4 @@
.save-helper { .save-helper {
width: 443px;
padding: 0; padding: 0;
flex-direction: column; flex-direction: column;
} }
@ -7,9 +6,9 @@
.save-title { .save-title {
padding: 10px; padding: 10px;
width: 100%; width: 100%;
background: linear-gradient(160deg, @green_primary, @green_secondary); background: linear-gradient(150deg, @green_primary, @green_secondary);
flex-direction: column; flex-direction: column;
border-radius: 3px 3px 0 0; border-radius: @panel_radius @panel_radius 0 0;
font-weight: 200; font-weight: 200;
box-sizing: border-box; box-sizing: border-box;
} }
@ -19,8 +18,8 @@
} }
.save-title-input { .save-title-input {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.3);
border-radius: 2px; border-radius: @panel_radius;
display: flex; display: flex;
input { input {
@ -72,11 +71,18 @@
.save-text { .save-text {
padding: 10px; padding: 10px;
line-height: 1.1em;
min-height: 2.2em;
} }
.save-buttons { .save-buttons {
display: flex; display: flex;
padding: 10px; padding: 0px;
margin-top: 20px;
.button {
margin-left: 10px;
}
} }
.save-buttons-text { .save-buttons-text {

View file

@ -1,5 +1,5 @@
const ru = [' ','\\.',',',':','\\?','#','Я','я','Ю','ю','Ч','ч','Ш','ш','Щ','щ','Ж','ж','А','а','Б','б','В','в','Г','г','Д','д','Е','е','Ё','ё','З','з','И','и','Й','й','К','к','Л','л','М','м','Н','н', 'О','о','П','п','Р','р','С','с','Т','т','У','у','Ф','ф','Х','х','Ц','ц','Ы','ы','Ь','ь','Ъ','ъ','Э','э']; const ru = [' ','\\.',',',':','\\?','#','Я','я','Ю','ю','Ч','ч','Ш','ш','Щ','щ','Ж','ж','А','а','Б','б','В','в','Г','г','Д','д','Е','е','Ё','ё','З','з','И','и','Й','й','К','к','Л','л','М','м','Н','н', 'О','о','П','п','Р','р','С','с','Т','т','У','у','Ф','ф','Х','х','Ц','ц','Ы','ы','Ь','ь','Ъ','ъ','Э','э'];
const en = ['_','','','','','','Ya','ya','Yu','yu','Ch','ch','Sh','sh','Sh','sh','Zh','zh','A','a','B','b','V','v','G','g','D','d','E','e','E','e','Z','z','I','i','J','j','K','k','L','l','M','m','N','n', 'O','o','P','p','R','r','S','s','T','t','U','u','F','f','H','h','C','c','Y','y','`','`','\'','\'','E', 'e']; const en = ['_','','','','','','Ya','ya','Yu','yu','Ch','ch','Sh','sh','Sh','sh','Zh','zh','A','a','B','b','V','v','G','g','D','d','E','e','E','e','Z','z','I','i','J','j','K','k','L','l','M','m','N','n', 'O','o','P','p','R','r','S','s','T','t','U','u','F','f','H','h','C','c','Y','y','','','','','E', 'e'];
export const toHours = (info) => { export const toHours = (info) => {
const hrs = parseInt(Number(info), 10); const hrs = parseInt(Number(info), 10);