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

View file

@ -3,86 +3,63 @@ import { getUrlData, pushPath } from '$utils/history';
import { toTranslit } from '$utils/format';
import { TIPS } from '$constants/tips';
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) {
super(props);
this.state = {
address: props.address || '',
title: props.title || '',
error: '',
sending: false,
finished: false,
overwriting: false,
};
}
getAddress = () => {
const { path } = getUrlData();
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 || '') });
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) => {
const { route, stickers } = this.props.editor.dumpData();
const { title } = this.state;
const { id, token } = this.props.user;
const address = this.getAddress();
postMap({
id,
token,
route,
stickers,
title,
force,
address: this.getAddress(),
}).then(this.parseResponse).catch(console.warn);
this.props.sendSaveRequest({
title, address, force,
});
};
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() {
const {
title, error, finished, overwriting, sending
} = this.state;
const { title } = this.state;
const { save_error, save_finished, save_overwriting, save_processing } = this.props;
const { host } = getUrlData();
return (
@ -90,42 +67,40 @@ export class SaveDialog extends React.Component {
<div className="save-title">
<div className="save-title-input">
<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 className="save-description">
<div className="save-address-input">
<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 className="save-text">
{
error || TIPS.SAVE_INFO
}
{ save_error || TIPS.SAVE_INFO }
</div>
<div className="save-buttons">
<div className="save-buttons-text" />
<div className={classnames({ 'button-group': !finished })}>
<div>
{ !finished &&
<div className="button" onClick={this.cancelSaving}>Отмена</div>
{ !save_finished &&
<div className="button" onClick={this.cancelSaving}>Отмена</div>
}
{
(!sending || (sending && !overwriting && !finished)) &&
!save_finished && !save_overwriting &&
<div className="button primary" onClick={this.sendSaveRequest}>Сохранить</div>
}
{
sending && overwriting &&
save_overwriting &&
<div className="button danger" onClick={this.forceSaveRequest}>Перезаписать</div>
}
{ finished &&
<div className="button success" onClick={this.cancelSaving}>Отлично, спасибо!</div>
{ save_finished &&
<div className="button success" onClick={this.cancelSaving}>Отлично, спасибо!</div>
}
</div>

View file

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

View file

@ -1,7 +1,7 @@
import { ACTIONS } from '$redux/user/constants';
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 });
@ -24,3 +24,11 @@ export const clearPoly = () => ({ type: ACTIONS.CLEAR_POLY });
export const clearStickers = () => ({ type: ACTIONS.CLEAR_STICKERS });
export const clearAll = () => ({ type: ACTIONS.CLEAR_ALL });
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_ALL: 'CLEAR_ALL',
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 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 setDistance = (state, { distance }) => ({
...state,
@ -33,6 +35,17 @@ const setLogo = (state, { logo }) => ({ ...state, logo });
const setTitle = (state, { title }) => ({ ...state, title });
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 = {
[ACTIONS.SET_USER]: setUser,
[ACTIONS.SET_EDITING]: setEditing,
@ -44,6 +57,11 @@ const HANDLERS = {
[ACTIONS.SET_LOGO]: setLogo,
[ACTIONS.SET_TITLE]: setTitle,
[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 = {
@ -58,6 +76,11 @@ export const INITIAL_STATE = {
title: 0,
address: '',
changed: false,
save_error: '',
save_finished: false,
save_overwriting: false,
save_processing: false,
};
export const userReducer = createReducer(INITIAL_STATE, HANDLERS);

View file

@ -1,12 +1,22 @@
import { REHYDRATE } from 'redux-persist';
import { takeLatest, select, call, put, takeEvery } from 'redux-saga/effects';
import { checkUserToken, getGuestToken, getStoredMap } from '$utils/api';
import { setActiveSticker, setChanged, setEditing, setMode, setUser } from '$redux/user/actions';
import { delay } from 'redux-saga';
import { takeLatest, select, call, put, takeEvery, race, take } from 'redux-saga/effects';
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 { editor } from '$modules/Editor';
import { ACTIONS } from '$redux/user/constants';
import { MODES } from '$constants/modes';
import { DEFAULT_USER } from '$constants/auth';
import { TIPS } from '$constants/tips';
const getUser = state => (state.user.user);
const getState = state => (state.user);
@ -172,6 +182,41 @@ function* clearSaga({ type }) {
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() {
// ASYNCHRONOUS!!! :-)
@ -194,4 +239,6 @@ export function* userSaga() {
ACTIONS.CLEAR_CANCEL,
], clearSaga);
yield takeLatest(ACTIONS.SEND_SAVE_REQUEST, sendSaveRequestSaga);
yield takeLatest(ACTIONS.SET_SAVE_SUCCESS, setSaveSuccessSaga);
}

View file

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

View file

@ -1,5 +1,5 @@
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) => {
const hrs = parseInt(Number(info), 10);