finished draft of intermediary refactor of addtostudio modal redux code, container-presentation code simplified

This commit is contained in:
Ben Wheeler 2018-07-13 09:50:17 -04:00
parent 6a32edb2fe
commit bc7b31e924
5 changed files with 118 additions and 175 deletions

View file

@ -1,18 +1,3 @@
// NOTE: next questions:
// * what is the lifecycle of the getStudios etc. requests? Are they guaranteed to be there
// on page load? Are they ever updated, e.g. after you join one?
// design decisions:
// * we should treat "waiting" to mean, user has requested the modal to be closed;
// that is, if you click ok and it's waiting for responses, then you click x,
// it closes and sets waiting to false?
// then in the checkForOutstandingUpdates function, we close the window
// iff waiting is true.
// that avoids the situation where you close the window while a request is
// outstanding, then reopen it only to have it instantly close on you.
// * keep the okay button, it sets up an overall spinner until everything is resolved
// * but you can totally close the window regardless
// sample data:
// this.studios = [{name: 'Funny games', id: 1}, {name: 'Silly ideas', id: 2}];
// studios data is like:
@ -30,10 +15,6 @@ const bindAll = require('lodash.bindall');
const truncate = require('lodash.truncate');
const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const log = require('../../../lib/log.js');
const Form = require('../../forms/form.jsx');
@ -42,8 +23,8 @@ const Select = require('../../forms/select.jsx');
const Spinner = require('../../spinner/spinner.jsx');
const TextArea = require('../../forms/textarea.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');
const AddToStudioModalPresentation = require('presentation.jsx');
const RequestStatus = require('').intlShape;
const AddToStudioModalPresentation = require('./presentation.jsx');
const previewActions = require('../../../redux/preview.js');
require('../../forms/button.scss');
require('./modal.scss');
@ -51,126 +32,61 @@ require('./modal.scss');
class AddToStudioModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [ // NOTE: will need to add and bind callback fn to handle addind and removing studios
'handleToggle',
'handleRequestClose',
bindAll(this, [
'handleSubmit'
]);
// NOTE: need to:
// construct hash of inclusion status by id, populate it.
// replace curatedStudios with list of studios ordered by
// membership/stats/name. use that for rendering.
this.state = {
waitingToClose: false,
studioState: {},
waitingToClose: false
};
}
componentDidMount() {
this.updateStudioState(this.props.projectStudios, this.props.curatedStudios);
componentWillUpdate (prevProps) {
checkIfFinishedUpdating();
}
componentWillReceiveProps(nextProps) {
this.updateStudioState(nextProps.projectStudios, nextProps.curatedStudios);
hasOutstandingUpdates () {
return (studios.some(studio => (studio.hasRequestOutstanding === true)));
}
updateStudioState(projectStudios, curatedStudios) {
// can't just use the spread operator here, because we may have
// project studios removed from the list.
// NOTE: This isn't handling the removal of a studio from the list well.
// can't build it from scratch, because needs transitional state
let studioState = Object.assign({}, this.state.studioState);
// remove all states that are not in transition
for (let id in studioState) {
if (studioState[id] === AddToStudioModalPresentation.State.IN
|| studioState[id] === AddToStudioModalPresentation.State.OUT) {
delete studioState[id];
}
}
// for all studios with no state, either because they weren't transitional
// or they're new, start them with state OUT
curatedStudios.forEach((curatedStudio) => {
if (!(curatedStudio.id in studioState)) {
studioState[curatedStudio.id] = AddToStudioModalPresentation.State.OUT
}
});
// nests which all states to in for studios this project is in
projectStudios.forEach((joinedStudio) => {
studioState[joinedStudio.id] = AddToStudioModalPresentation.State.IN
});
// NOTE: do I really need this assign? I took it out
this.setState({studioState: studioState});
}
handleToggle(studioId) {
if (studioId in this.state.studioState) {
const studioState = this.state.studioState[studioId];
// ignore clicks on studio buttons that are still waiting for response
if (studioState === AddToStudioModalPresentation.State.IN) {
this.props.onToggleStudio(studioId, false)
} elseif (studioState === AddToStudioModalPresentation.State.OUT) {
this.props.onToggleStudio(studioId, true)
}
} else {
// NOTE: error
checkIfFinishedUpdating () {
if (waitingToClose === true && hasOutstandingUpdates() === false) {
this.setState({waitingToClose: false}, () => {
this.props.onRequestClose();
});
}
}
// we need to separately handle
// server responses to our update requests,
// and after each one, check to see if there are no outstanding updates
// queued.
checkForOutstandingUpdates () {
const updateQueued = this.state.updateQueued;
if (Object.keys(updateQueued).length == 0) {
setTimeout(function() {
this.setState({waitingToClose: false}, () => {
this.handleRequestClose();
});
}.bind(this), 3000);
}
}
handleRequestClose () {
// NOTE that we do NOT clear joined, so we don't lose
// user's work from a stray click outside the modal...
// but maybe this should be different?
this.baseModal.handleRequestClose();
}
handleSubmit (formData) {
// NOTE:For this approach to work, we need to separately handle
// server responses to our update requests,
// and after each one, check to see if there are no outstanding updates
// queued.
this.setState({waitingToClose: true}, () => {
this.checkForOutstandingUpdates();
this.checkIfFinishedUpdating();
});
}
render () {
const {
curatedStudios,
isOpen,
studios,
onToggleStudio,
onRequestClose
} = this.props;
return (
<AddToStudioModalPresentation
studios={curatedStudios}
studioState={this.state.studioState}
studios={studios}
isOpen={isOpen}
onToggleStudio={handleToggle}
onToggleStudio={onToggleStudio}
onSubmit={handleSubmit}
/>
);
}
}
AddToStudioModal.propTypes = {
projectStudios: PropTypes.arrayOf(PropTypes.object),
curatedStudios: PropTypes.arrayOf(PropTypes.object),
studioRequests: PropTypes.object,
isOpen: PropTypes.bool,
studios: PropTypes.arrayOf(PropTypes.object),
onToggleStudio: PropTypes.func,
onRequestClose: PropTypes.func
};
module.exports = AddToStudioModalPresentation;

View file

@ -1,4 +1,3 @@
const keyMirror = require('keymirror');
const bindAll = require('lodash.bindall');
const truncate = require('lodash.truncate');
const PropTypes = require('prop-types');
@ -19,39 +18,24 @@ const FlexRow = require('../../flex-row/flex-row.jsx');
require('../../forms/button.scss');
require('./modal.scss');
module.exports.State = keyMirror({
IN: null,
OUT: null,
JOINING: null,
LEAVING: null
});
class AddToStudioModalPresentation extends React.Component {
constructor (props) {
super(props);
bindAll(this, [ // NOTE: will need to add and bind callback fn to handle addind and removing studios
'handleRequestClose',
bindAll(this, [
'handleSubmit'
]);
// NOTE: need to:
// construct hash of inclusion status by id, populate it.
// replace curatedStudios with list of studios ordered by
// membership/stats/name. use that for rendering.
this.state = {
waitingToClose: false,
studios: props.studios
};
}
handleRequestClose () {
this.baseModal.handleRequestClose();
}
handleSubmit (formData) {
this.props.handleSubmit(formData);
this.props.onSubmit(formData);
}
render () {
// NOTE: how does intl get injected?
const {
intl,
studios,
@ -68,29 +52,24 @@ class AddToStudioModalPresentation extends React.Component {
src="/svgs/modal/add.svg"
/>
const studioButtons = studios.map((studio, index) => {
const thisStudioState = studioState[studio.id];
return (
<div className={"studio-selector-button " +
(thisStudioState === module.exports.State.JOINING ||
thisStudioState === module.exports.State.LEAVING) ?
"studio-selector-button-waiting" :
(thisStudioState === module.exports.State.IN ? "studio-selector-button-selected" : ""))}
(studio.hasRequestOutstanding ? "studio-selector-button-waiting" :
(studio.includesProject ? "studio-selector-button-selected" : ""))}
key={studio.id}
onClick={() => this.props.onToggleStudio(studio.id)}
>
<div className={"studio-selector-button-text " +
(thisStudioState === module.exports.State.OUT ?
"studio-selector-button-text-unselected" :
".studio-selector-button-text-selected")}>
(studio.includesProject ? "studio-selector-button-text-selected" :
"studio-selector-button-text-unselected")}>
{truncate(studio.title, {'length': 20, 'separator': /[,:\.;]*\s+/})}
</div>
<div className={"studio-status-icon " +
(thisStudioState === module.exports.State.OUT ? "studio-status-icon-unselected" : "")}
(studio.includesProject ? "" : "studio-status-icon-unselected")}
>
{(thisStudioState === module.exports.State.JOINING ||
thisStudioState === module.exports.State.LEAVING) ?
{(studio.hasRequestOutstanding ?
(<Spinner type="smooth" />) :
(thisStudioState === module.exports.State.IN ? checkmark : plus)}
(studio.includesProject ? checkmark : plus))}
</div>
</div>
);
@ -100,9 +79,6 @@ class AddToStudioModalPresentation extends React.Component {
<Modal
className="mod-addToStudio"
contentLabel={contentLabel}
ref={component => { // bind to base modal, to pass handleRequestClose through
this.baseModal = component;
}}
onRequestClose={this.props.onRequestClose}
isOpen={isOpen}
>
@ -131,7 +107,7 @@ class AddToStudioModalPresentation extends React.Component {
<FlexRow className="action-buttons">
<Button
className="action-button close-button white"
onClick={this.handleRequestClose}
onClick={this.props.onRequestClose}
key="closeButton"
name="closeButton"
type="button"
@ -177,7 +153,8 @@ AddToStudioModalPresentation.propTypes = {
intl: intlShape,
studios: PropTypes.arrayOf(PropTypes.object),
onAddToStudio: PropTypes.func,
onRequestClose: PropTypes.func
onRequestClose: PropTypes.func,
onSubmit: PropTypes.func
};
module.exports = injectIntl(AddToStudioModalPresentation);

View file

@ -31,7 +31,7 @@ module.exports.getInitialState = () => ({
original: {},
parent: {},
projectStudios: [],
curatedStudios: [],
curatedStudios: []
});
module.exports.previewReducer = (state, action) => {
@ -64,6 +64,17 @@ module.exports.previewReducer = (state, action) => {
return Object.assign({}, state, {
curatedStudios: action.items
});
case 'ADD_TO_PROJECT_STUDIOS':
return Object.assign({}, state, {
// NOTE: move this to calling fn, make this add object passed to me
projectStudios: state.projectStudios.concat({id: action.studioId})
});
case 'REMOVE_FROM_PROJECT_STUDIOS':
return Object.assign({}, state, {
projectStudios: state.projectStudios.filter(studio => (
studio.id !== action.studioId
))
});
case 'SET_COMMENTS':
return Object.assign({}, state, {
comments: action.items
@ -137,7 +148,15 @@ module.exports.setCuratedStudios = items => ({
items: items
});
// NOTE: unclear to me what kind of Delta to do to what data when add and leave commands come back
module.exports.addToProjectStudios = studioId => ({
type: 'ADD_TO_PROJECT_STUDIOS',
studioId: studioId
});
module.exports.removeFromProjectStudios = studioId => ({
type: 'REMOVE_FROM_PROJECT_STUDIOS',
studioId: studioId
});
module.exports.setFetchStatus = (type, status) => ({
type: 'SET_FETCH_STATUS',
@ -405,6 +424,7 @@ module.exports.getCuratedStudios = (username, token) => (dispatch => {
});
module.exports.addToStudio = (studioId, projectId, token) => (dispatch => {
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHING));
api({
uri: `/studios/${studioId}/project/${projectId}`,
authentication: token,
@ -419,11 +439,15 @@ module.exports.addToStudio = (studioId, projectId, token) => (dispatch => {
return;
}
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
// NOTE: is there a way here to update or refresh the project studio list?
// action: add studio to list
// NOTE: what is the content of the body in the response to this request?
// should we pass the rich object to addToProjectStudios ?
dispatch(module.exports.addToProjectStudios(studioId));
});
});
module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHING));
api({
uri: `/studios/${studioId}/project/${projectId}`,
authentication: token,
@ -438,7 +462,7 @@ module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
return;
}
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
// NOTE: is there a way here to update or refresh the project studio list?
dispatch(module.exports.removeFromProjectStudios(studioId));
});
});

View file

@ -221,7 +221,6 @@ class PreviewPresentation extends React.Component {
sessionStatus,
projectStudios,
curatedStudios,
studioRequests,
onToggleStudio,
user,
onFavoriteClicked,
@ -474,9 +473,7 @@ class PreviewPresentation extends React.Component {
<AddToStudioModal
isOpen={this.state.addToStudioOpen}
key="add-to-studio-modal"
projectStudios={projectStudios}
curatedStudios={this.mockedMyStudios}
studioRequests={studioRequests}
studios={curatedStudios}
onToggleStudio={onToggleStudio}
onRequestClose={this.handleAddToStudioClose}
/>
@ -592,7 +589,6 @@ PreviewPresentation.propTypes = {
sessionStatus: PropTypes.string.isRequired,
projectStudios: PropTypes.arrayOf(PropTypes.object),
curatedStudios: PropTypes.arrayOf(PropTypes.object),
studioRequests: PropTypes.object,
onToggleStudio: PropTypes.func,
user: PropTypes.shape({
id: PropTypes.number,

View file

@ -23,6 +23,7 @@ class Preview extends React.Component {
super(props);
bindAll(this, [
'addEventListeners',
'handleToggleStudio',
'handleFavoriteToggle',
'handleLoveToggle',
'handlePermissions',
@ -127,19 +128,12 @@ class Preview extends React.Component {
}
}
handleToggleStudio (studioId, isAdd) {
if (isAdd === true) {
this.props.addToStudio(
studioId,
this.props.projectInfo.id,
this.props.user.token
);
} else {
this.props.leaveStudio(
studioId,
this.props.projectInfo.id,
this.props.user.token
);
}
this.props.toggleStudio(
isAdd,
studioId,
this.props.projectInfo.id,
this.props.user.token
);
}
handleFavoriteToggle () {
this.props.setFavedStatus(
@ -218,7 +212,6 @@ class Preview extends React.Component {
sessionStatus={this.props.sessionStatus}
projectStudios={this.props.projectStudios}
curatedStudios={this.props.curatedStudios}
studioRequests={this.props.studioRequests}
onToggleStudio={this.handleToggleStudio}
user={this.props.user}
onFavoriteClicked={this.handleFavoriteToggle}
@ -249,6 +242,7 @@ Preview.propTypes = {
getRemixes: PropTypes.func.isRequired,
getProjectStudios: PropTypes.func.isRequired,
getCuratedStudios: PropTypes.func.isRequired,
toggleStudio: PropTypes.func.isRequired,
loved: PropTypes.bool,
original: projectShape,
parent: projectShape,
@ -262,7 +256,6 @@ Preview.propTypes = {
setPlayer: PropTypes.func.isRequired,
projectStudios: PropTypes.arrayOf(PropTypes.object),
curatedStudios: PropTypes.arrayOf(PropTypes.object),
studioRequests: PropTypes.object,
updateProject: PropTypes.func.isRequired,
user: PropTypes.shape({
id: PropTypes.number,
@ -281,6 +274,42 @@ Preview.defaultProps = {
user: {}
};
// Build consolidated curatedStudios object from all studio info.
// We add data to curatedStudios so it knows which of the studios the
// project belongs to, and the status of requests to join/leave studios.
function consolidateStudiosInfo (curatedStudios, projectStudios, studioRequests) {
let studios = [];
let projectStudiosFoundInCurated = {}; // temp, for time complexity
// copy curated studios, updating any that are also in other data structures
curatedStudios.forEach((curatedStudio) => {
let studioCopy = Object.assign({}, curatedStudio,
{includesProject: false, hasRequestOutstanding: false});
projectStudios.forEach((projectStudio) => {
if (curatedStudio.id === projectStudio.id) {
studioCopy.includesProject = true;
projectStudiosFoundInCurated[projectStudio.id] = true;
}
});
// set studio state to leaving or joining if it's being fetched
if (studioCopy.id in status.studioRequests) {
const request = status.studioRequests[studioId];
studioCopy.hasRequestOutstanding = (request === preview.Status.FETCHING);
}
studios.push(studioCopy);
});
// if there are any other studios this project is in that are NOT in the list
// of studios this user curates, like public studios, add to front of list
projectStudios.forEach((projectStudio) => {
if (!(projectStudio.id in projectStudiosFoundInCurated)) {
studios.unshift(Object.assign({}, projectStudio,
{includesProject: true, hasRequestOutstanding: false}
));
}
});
return studios;
}
const mapStateToProps = state => ({
projectInfo: state.preview.projectInfo,
comments: state.preview.comments,
@ -291,8 +320,8 @@ const mapStateToProps = state => ({
remixes: state.preview.remixes,
sessionStatus: state.session.status,
projectStudios: state.preview.projectStudios,
curatedStudios: state.preview.curatedStudios,
studioRequests: state.preview.status.studioRequests,
curatedStudios: consolidateStudiosInfo(state.preview.curatedStudios,
state.preview.projectStudios, state.preview.status.studioRequests),
user: state.session.session.user,
playerMode: state.scratchGui.mode.isPlayerOnly,
fullScreen: state.scratchGui.mode.isFullScreen
@ -318,11 +347,12 @@ const mapDispatchToProps = dispatch => ({
getCuratedStudios: (username, token) => {
dispatch(previewActions.getCuratedStudios(username, token));
},
addToStudio: (studioId, id, token) => {
dispatch(previewActions.addToStudio(studioId, id, token));
},
leaveStudio: (studioId, id, token) => {
dispatch(previewActions.leaveStudio(studioId, id, token));
toggleStudio: (isAdd, studioId, id, token) => {
if (isAdd === true) {
dispatch(previewActions.addToStudio(studioId, id, token));
} else {
dispatch(previewActions.leaveStudio(studioId, id, token));
}
},
getFavedStatus: (id, username, token) => {
dispatch(previewActions.getFavedStatus(id, username, token));