Я использую Redux в своем приложении React. В этом модуле у меня есть список объектов, которые при перемещении обновляют базу данных и добавляют весь список в массив местоположений рабочих столов. Я использую формат deskMoves[[],[],[]...]
. Каждый раз, когда я меняю стол, я добавляю обновленный список в стек. Затем я добавил обработчик клавиш для захвата ctrl + z, чтобы реализовать функцию отмены, а также добавил целочисленную переменную currentMove
в хранилище. При пошаговом выполнении кода все работает по плану, за исключением того, что экран не обновляется, пока я не нажму на последний измененный стол (который выделяется), после чего он переместится в предыдущее место. Код состоит из нескольких частей, поэтому я постараюсь разобрать их соответствующим образом (это очень сложное автономное приложение, работающее как модуль на более крупном сайте SPA)
//Layout.js
//I am going to include the entire page in case I have missed something obscure in the code
//The relevant functions should be handleKeyPress(), buildDesks() and render()
import React, { Component } from 'react';
// import Draggable from 'react-draggable';
// import FontAwesome from 'react-fontawesome';
import Draggable from '../Elements/Draggable';
import { connect } from 'react-redux';
import { Loading } from '../Elements/LoadingComponent';
import { Row, Col } from 'react-bootstrap';
import {
Layout_Get_Sites,
Layout_Get_Map_Background,
Layout_Set_Current_Site,
Layout_Get_Desk_Types,
Layout_Fetch_Desks,
Layout_Undo_Change,
Layout_Redo_Change,
Layout_Clear_Desk,
Layout_Delete_Desk,
Layout_Restore_All,
Layout_Update_Desk_Data,
Layout_Get_UserImages,
Layout_Create_Desk,
Layout_Set_Current_Desk
} from '../../redux/Creators/Layout_Creator';
import '../../shared/styles/layout.css';
const mapStateToProps = (state) => {
return {
layout: state.layout,
roles: state.siteMap.siteMapData.userRoles
}
}
const mapDispatchToProps = (dispatch) => {
//add action creators here - by reference?
return {
Layout_Set_Current_Site: (siteId) => { dispatch(Layout_Set_Current_Site(siteId)) },
Layout_Get_Sites: () => { dispatch(Layout_Get_Sites()) },
Layout_Get_Map_Background: (siteId) => { dispatch(Layout_Get_Map_Background(siteId)) },
Layout_Get_Desk_Types: () => { dispatch(Layout_Get_Desk_Types()) },
Layout_Fetch_Desks: (siteId) => { dispatch(Layout_Fetch_Desks(siteId)) },
Layout_Undo_Change: (render) => { dispatch(Layout_Undo_Change(render)) },
Layout_Redo_Change: (render) => { dispatch(Layout_Redo_Change(render)) },
Layout_Clear_Desk: (deskId) => { dispatch(Layout_Clear_Desk(deskId)) },
Layout_Delete_Desk: (deskId) => { dispatch(Layout_Delete_Desk(deskId)) },
Layout_Update_Desk_Data: (desk) => { dispatch(Layout_Update_Desk_Data(desk)) },
Layout_Get_UserImages: (deskId) => { dispatch(Layout_Get_UserImages(deskId)) },
Layout_Create_Desk: (type, siteId, height, width) => { dispatch(Layout_Create_Desk(type, siteId, height, width)) },
Layout_Restore_All: () => { dispatch(Layout_Restore_All()) },
Layout_Set_Current_Desk: (deskId) => { dispatch(Layout_Set_Current_Desk(deskId)) }
};
}
class LayoutMap extends Component {
constructor(props) {
super(props);
this.state = {
width: 0,
edit: false,
details: '',
deskStatus: { top: 0, left: 0 }
};
}
componentDidMount() {
//begin initial load after component is built
window.addEventListener('resize', this.handleResize)
this.props.Layout_Restore_All();
this.props.Layout_Get_Sites();
this.props.Layout_Get_Desk_Types();
this.handleResize();
document.addEventListener('keypress', this.handleKeyPress)
}
/************************************************ */
// Utility Functions //
/************************************************ */
changeMap = (target) => {
this.clickDesk(null, null);
const site = parseInt(target.value);
this.props.Layout_Set_Current_Site(site);
const siteId = this.props.layout.maps[site].id;
this.props.Layout_Get_Map_Background(siteId);
this.props.Layout_Fetch_Desks(siteId);
}
getScale = () => {
const currentMap = this.props.layout.currentMap;
const map = this.props.layout.maps[currentMap];
const scale = parseFloat(map.scale);
const wd = parseInt(this.state.width / 12 * 10);
const realWd = map.Width * scale;
const newScale = wd / realWd;
return newScale;
}
getDesks = () => {
const desks = [...this.props.layout.deskMoves[this.props.layout.currentMove]];
return desks;
}
updateStats = () => {
alert("Update");
}
clearUserStats = () => {
alert("Clear Stats");
}
checkChanged = (e) => {
let check = e.target;
this.setState({ edit: check.checked })
}
updateProperties = (data) => {
let string = `Top: ${data.top}, Left:${data.left}`;
// data = this.state.details + ', ' + data
this.setState({ details: string });
}
/************************************************ */
// Event Handlers //
/************************************************ */
handleResize = () => {
this.setState({
width: window.innerWidth,
deskStatus: null
});
}
handleKeyPress = (e) => {
if (this.state.edit) {
switch (e.code) {
case 'KeyZ':
if (e.ctrlKey) {
this.props.Layout_Undo_Change(this.forceUpdate);
e.cancelBubble = true;
}
break;
case 'KeyY':
if (e.ctrlKey) {
this.props.Layout_Redo_Change(this.forceUpdate);
e.cancelBubble = true;
}
break;
default:
break;
}
}
}
mouseUp = (e, deskId, data) => {
const desks = this.getDesks();
let desk = desks[deskId];
if (data.dragged && this.state.edit) {
this.clickDesk(e, deskId);
const scale = this.getScale();
const newX = parseInt(data.left / scale);
const newY = parseInt(data.top / scale);
desk.x = newX + "";
desk.y = newY + "";
this.props.Layout_Update_Desk_Data(desk);
}
else {
this.clickDesk(e, deskId);
}
}
clickDesk = (e, deskId) => {
if (deskId !== null && deskId !== undefined && deskId !== false) {
const desks = this.getDesks();
let desk = desks[deskId];
this.props.Layout_Set_Current_Desk(desk);
}
else {
this.props.Layout_Set_Current_Desk(null);
}
}
rightClick = (e, deskId) => {
if (this.state.edit) {
const desks = this.getDesks();
const desk = desks[deskId];
let rotation = parseInt(desk.rotation);
rotation += 90;
if (rotation >= 360) rotation -= 360;
desk.rotation = rotation;
this.props.Layout_Set_Current_Desk(desk);
this.props.Layout_Update_Desk_Data(desk);
}
}
/************************************************ */
// Drawing Functions //
/************************************************ */
showAdmin = () => {
const roles = this.props.roles;
if (roles.toLowerCase().indexOf('admin') >= 0) {
return (<span style={{ 'whiteSpace': 'nowrap', 'backgroundColor': '#ccc' }}>
<label htmlFor='layoutEditSelect'>Edit</label>
<input id='layoutEditSelect' type='checkbox' onClick={(e) => this.checkChanged(e)}
/>
</span>
);
}
else {
return <span></span>
}
}
buildMapOptions = () => {
var ret = this.props.layout.maps.map((site, index) => {
return (<option value={index} key={index}>{site.SiteName}</option>);
});
return (
<React.Fragment>
<option id='0'>Select Site...</option>
{ret}
</React.Fragment>
);
}
buildDesks = () => {
const newScale = this.getScale();
const layout = this.props.layout;
const desks = this.getDesks();
let ret = desks.map((desk, index) => {
let deskImg = null;
try {
let dImg = layout.deskTypes.find(
d => parseInt(d.deskType) === parseInt(desk.deskType)
);
deskImg = dImg.deskImage;
}
catch (ex) {
console.log(ex);
}
const userName = desk.UserLogon !== (null || '') ? desk.UserLogon : "Unassigned";
const top = Math.trunc(parseInt(parseInt(desk.y) * newScale));
const left = Math.trunc(parseInt(parseInt(desk.x) * newScale));
let imgStyle = {
width: `${parseInt(parseInt(desk.width) * newScale)}px`,
height: `${parseInt((parseInt(desk.height) * newScale))}px`,
transform: `rotate(${parseInt(desk.rotation)}deg)`,
position: 'absolute'
}
if (layout.currentDesk && desk.id === layout.currentDesk.id) {
imgStyle.border = '2px solid cyan';
}
const url = `data:image/jpeg;base64,${deskImg}`;
try {
return (
<Draggable key={desk.id}
index={index}
enabled={this.state.edit}
left={left}
top={top}
onMove={this.updateProperties}
onStop={this.mouseUp}
onRightClick={this.rightClick}
>
<div style={{ position: 'relative' }} className='deskImg'>
<img style={imgStyle} alt={userName} src={url} />
</div>
</Draggable>
);
}
catch (ex) {
console.log(ex);
return null;
}
});//desks.map
// this.clickDesk(null);
return ret;
}//buildDesks
buildMap = () => {
let url = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
try {
let currentMap = this.props.layout.currentMap;
let map = this.props.layout.maps[currentMap];
let mapImage = map.SiteBackground;
//use map image once it is loaded
if (mapImage !== undefined) {
url = 'data:image/jpeg;base64,' + mapImage;
}
}
catch (ex) {
console.log(ex);
}
return (
<React.Fragment>
<img id="Layout_SiteMap_Img"
style={{
width: '100%',
height: '100%',
}}
src={url}
alt=''
onClick={(e) => this.clickDesk(e, false)}
/>
</React.Fragment>
)
}
showStatus = () => {
let desk = this.props.layout.currentDesk;
if (desk === null) return (<div></div>);
const visible = desk !== null ? 'visible' : 'hidden';
let left = parseInt(window.innerWidth - 225);
return (
<div className='editData'
style={{ left: left + 'px', visibility: visible }}>
<Row className='statusRow'>
<Col sm={12}>
<div id="Layout_UserImg" title="Click to upload new user image" className='Layout_userImg'>
<div id="Layout_UserImgLabel">
Click to
upload new
user image</div>
</div>
</Col>
</Row>
<Row className='statusRow'>
<Col sm={5} className='statusDivHeading'><label htmlFor='Layout_Manager'>Manager</label></Col>
<Col sm={7} className='statusDivData Layout_Text_Input' contentEditable={true}
suppressContentEditableWarning={true} id="Layout_Manager">
{desk.Manager}
</Col>
</Row>
<Row className='statusRow'>
<Col sm={5} className='statusDivHeading'><label htmlFor='Layout_User'>User</label></Col>
<Col sm={7} className='statusDivData Layout_Text_Input' contentEditable={true}
suppressContentEditableWarning={true} id="Layout_User">
{desk.UserName}
</Col>
</Row>
<Row className='statusRow'>
<Col sm={5} className='statusDivHeading'><label htmlFor='Layout_EmpId'>Emp. Id</label></Col>
<Col sm={7} className='statusDivData Layout_Text_Input' contentEditable={true}
suppressContentEditableWarning={true} id="Layout_EmpId">
{desk.UserLogon}
</Col>
</Row>
<Row className='statusRow'>
<Col sm={5} className='statusDivHeading'><label htmlFor='Layout_Extension'>Extension</label></Col>
<Col sm={7} className='statusDivData Layout_Text_Input' contentEditable={true}
suppressContentEditableWarning={true} id="Layout_Extension">
{desk.Extension}
</Col>
</Row>
<Row className='statusRow'>
<Col sm={5} className='statusDivHeading'><label htmlFor='Layout_Department'>Department</label></Col>
<Col sm={7} className='statusDivData Layout_Text_Input' contentEditable={true}
suppressContentEditableWarning={true} id="Layout_Department">
{desk.Department}
</Col>
</Row>
<Row className='statusRow'>
<Col sm={5} className='statusDivHeading'><label htmlFor='Layout_DBRowId'>DB Id</label></Col>
<Col sm={7} className='statusDivData Layout_Text_Input' contentEditable={true}
suppressContentEditableWarning={true} id="Layout_DBRowId">
{desk.id}
</Col>
</Row>
<Row className='statusRow'>
<Col sm={5} className='statusDivHeading'><label htmlFor='Layout_DeskID'>Desk ID</label></Col>
<Col sm={7} className='statusDivData Layout_Text_Input' contentEditable={true}
suppressContentEditableWarning={true} id="Layout_DeskID">
{desk.DeskID}
</Col>
</Row>
<Row className='statusRow'>
<Col sm={5} className='statusDivHeading'><label htmlFor='Layout_Assets'>Assets</label></Col>
<Col sm={7} className='statusDivData Layout_Text_Input' contentEditable={true}
suppressContentEditableWarning={true} id="Layout_Assets">
{desk.Assets}
</Col>
</Row>
<Row>
<Col offset={1} sm={5} className='Layout_Button_Col '>
<button onClick={this.updateStats}>Save</button>
</Col>
<Col sm={5} style={{ textAlign: 'left' }} className='Layout_Button_Col ' >
<button onClick={this.clearUserStats}>Clear</button>
</Col>
</Row >
</div >
)
}
render = () => {
if (this.props.layout.isLoading) {
return (<Loading title="Site Layout" />);
}
else if (this.props.layout.isLoadingMap) {
const map = this.props.layout.maps[this.props.layout.currentMap];
const siteName = map.SiteName;
return (
<Row>
<Col sm={1}></Col>
<Col sm={10} id="Layout_Map_Container">
<Loading title={"map '" + siteName + "'"} />
</Col>
</Row>
);
}
else if (this.props.layout.mapLoaded) {
return (
<div>
<Row>
<Col sm={1}>
{this.showAdmin()}
</Col>
<Col sm={10}>
{this.state.details} {this.props.layout.currentMove}
</Col>
</Row>
<Row>
<Col sm={1}>
<select onChange={(e) => this.changeMap(e.target)}>
{this.buildMapOptions()}
</select>
</Col>
<Col sm={10} id="Layout_Map_Container">
{this.buildMap()}
{this.buildDesks()}
</Col>
</Row >
{this.showStatus()}
</div>
);
}
else {
return (
<Row>
<Col sm={1}>
<select onChange={(e) => this.changeMap(e.target)}>
{this.buildMapOptions()}
</select>
</Col>
<Col sm={10} id="Layout_Map_Container">
</Col>
</Row>
);
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LayoutMap);
//Layout_Creator.js
//only including the functions that get called in this process
export const Layout_Undo_Change = (render) => (dispatch, getState) => {
const state = getState();
let currentMove = state.layout.currentMove;
// let currentDesks = { ...state.layout.deskMoves[currentMove] };
if (currentMove > 0)
currentMove--;
let newDesks = state.layout.deskMoves[currentMove];
let init = fetchInit();
init.method = "POST";
const deskData = { mode: 'UPDATEMANY', data: newDesks };
init.body = JSON.stringify(deskData);
let myReq = new Request(`/dataAPI/Layout/`, init);
fetch(myReq)
.then((response) => {
if (response.ok) {
return response;
}
else {
var error = new Error("Error " + response.statusText);
error.response = response;
throw error;
}
}, (error) => {
var err = new Error(error.message);
throw err;
})
.then((response) => { return response.json() })
.then((data) => {
try {
data = JSON.parse(data); //make sure we have an object not a string
}
catch (ex) {
}
finally {
dispatch({
type: ActionTypes.LAYOUT_SET_MOVES,
payload: currentMove
});
render();
}
})
.catch(err => {
return dispatch({
type: ActionTypes.LAYOUT_FAILED,
payload: err.message
});
});
}
//not sure if this function should prove relevant
export const Layout_Fetch_Desks = (siteId, current = null) => (dispatch, getState) => {
//get all data for desk - should come from site
//fetch numdesks
var init = fetchInit();
let req = new Request(`/dataAPI/Layout/GETDESKS?siteId=${siteId}`, init);
fetch(req)
.then((response) => {
if (response.ok) {
return response;
}
else {
var error = new Error("Error " + response.statusText);
error.response = response;
throw error;
}
}, (error) => {
var err = new Error(error.message);
throw err;
})
.then((deskResponse) => { return deskResponse.json() })
.then((desk) => {
try {
const state = getState();
let moves = [...state.layout.deskMoves];
if (current === null) {
moves.push(desk.desks);
// moves.length = current; //if this is an update by keypress delete additional
current = moves.length - 1;
dispatch({ //set the most current set of desks
type: ActionTypes.LAYOUT_UPDATE_MOVES,
payload: moves
})
}
return dispatch({ //set which set of desks to use
type: ActionTypes.LAYOUT_SET_MOVES,
payload: current
})
}
catch (ex) {
return null;
}
})
.catch((err) => {
dispatch({
type: ActionTypes.LAYOUT_FAILED,
payload: err.message
})
});
}
//Layout_Reducer.js
import * as ActionTypes from '../ActionTypes';
export const layout = (state = {
isLoading: true,
isLoadingMap: false,
mapLoaded: false,
currentMap: null,
currentDesk: null,
maps: [],
desks: [],
deskTypes: [],
deskMoves: [],
currentMove: 0,
selectedDesk: null,
editMode: false,
errMess: null
}, action) => {
switch (action.type) {
case ActionTypes.LAYOUT_SITES_LOADING:
return { ...state, isLoading: true };
case ActionTypes.LAYOUT_DESKS_LOADING:
return { ...state, isLoadingDesks: true, desks: [] };
case ActionTypes.LAYOUT_MAP_LOADING:
return {
...state, isLoadingMap: true, desks: [],
selectedDesk: null, editMode: false
};
case ActionTypes.LAYOUT_MAP_LOADED:
return { ...state, isLoadingMap: false, mapLoaded: true, maps: action.payload };
case ActionTypes.LAYOUT_MAPS_LOADED:
return { ...state, maps: action.payload, isLoading: false };
case ActionTypes.LAYOUT_DESKTYPES_LOADED:
return { ...state, deskTypes: action.payload };
case ActionTypes.LAYOUT_DESK_LOADED:
return { ...state, desks: action.payload };
case ActionTypes.LAYOUT_SET_SELECTED_DESK:
return { ...state, selectedDesk: action.payload };
case ActionTypes.LAYOUT_SET_EDITMODE:
return { ...state, editMode: action.payload };
case ActionTypes.LAYOUT_DESK_DELETED:
return { ...state, desks: action.payload, selectedDesk: null }
case ActionTypes.LAYOUT_SET_CURRENT_DESK:
return { ...state, currentDesk: action.payload };
case ActionTypes.LAYOUT_SET_ACTIVE_MAP:
return { ...state, currentMap: action.payload };
case ActionTypes.LAYOUT_UPDATE_MOVES:
return { ...state, deskMoves: action.payload };
case ActionTypes.LAYOUT_SET_MOVES:
return { ...state, currentMove: action.payload };
case ActionTypes.LAYOUT_FAILED:
return {
...state, isLoadingMap: false, isLoadingDesks: false,
errMess: action.payload, pageUsageData: []
};
case ActionTypes.LAYOUT_RESTORE_ALL:
return {
...state,
isLoading: true, isLoadingMap: false, mapLoaded: false, currentMap: null,
maps: [], desks: [], deskTypes: [], deskMoves: [], currentMove: 0,
selectedDesk: null, editMode: false, errMess: null
}
default:
return state;
}
}
Где-то в этот беспорядок кода есть причина того, что что-то не обновляется правильно. Мое подозрение заключается в асинхронной природе React, но я не уверен, как это обойти.