React redux Undo Stack не обновляет элементы - PullRequest
0 голосов
/ 05 августа 2020

Я использую 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>&nbsp;
                <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, но я не уверен, как это обойти.

...