adding distinct object to redux/preview.js state to track studio membership

This commit is contained in:
Ben Wheeler 2018-07-24 09:12:13 -04:00
parent 52281502b2
commit ffe5e8cb43
3 changed files with 138 additions and 174 deletions

View file

@ -17,123 +17,117 @@ const FlexRow = require('../../flex-row/flex-row.jsx');
require('../../forms/button.scss'); require('../../forms/button.scss');
require('./modal.scss'); require('./modal.scss');
class AddToStudioModalPresentation extends React.Component { const AddToStudioModalPresentation = ({
constructor (props) { isOpen,
super(props); studios,
bindAll(this, [ waitingToClose,
'handleSubmit' onToggleStudio,
]); onRequestClose,
} onSubmit
}) => {
handleSubmit (formData) { const contentLabel = this.props.intl.formatMessage({id: "addToStudio.title"});
this.props.onSubmit(formData); const checkmark = <img alt="checkmark-icon"
} className="studio-status-icon-checkmark-img"
src="/svgs/modal/confirm.svg"
render () { />
const contentLabel = this.props.intl.formatMessage({id: "addToStudio.title"}); const plus = <img alt="plus-icon"
const checkmark = <img alt="checkmark-icon" className="studio-status-icon-plus-img"
className="studio-status-icon-checkmark-img" src="/svgs/modal/add.svg"
src="/svgs/modal/confirm.svg" />
/> const studioButtons = this.props.studios.map((studio, index) => {
const plus = <img alt="plus-icon" return (
className="studio-status-icon-plus-img" <div className={"studio-selector-button " +
src="/svgs/modal/add.svg" (studio.hasRequestOutstanding ? "studio-selector-button-waiting" :
/> (studio.includesProject ? "studio-selector-button-selected" : ""))}
const studioButtons = this.props.studios.map((studio, index) => { key={studio.id}
return ( onClick={() => this.props.onToggleStudio(studio.id)}
<div className={"studio-selector-button " + >
(studio.hasRequestOutstanding ? "studio-selector-button-waiting" : <div className={"studio-selector-button-text " +
(studio.includesProject ? "studio-selector-button-selected" : ""))} (studio.includesProject ? "studio-selector-button-text-selected" :
key={studio.id} "studio-selector-button-text-unselected")}>
onClick={() => this.props.onToggleStudio(studio.id)} {truncate(studio.title, {'length': 20, 'separator': /[,:\.;]*\s+/})}
</div>
<div className={"studio-status-icon " +
(studio.includesProject ? "" : "studio-status-icon-unselected")}
> >
<div className={"studio-selector-button-text " + {(studio.hasRequestOutstanding ?
(studio.includesProject ? "studio-selector-button-text-selected" : (<Spinner type="smooth" />) :
"studio-selector-button-text-unselected")}> (studio.includesProject ? checkmark : plus))}
{truncate(studio.title, {'length': 20, 'separator': /[,:\.;]*\s+/})} </div>
</div> </div>
<div className={"studio-status-icon " + );
(studio.includesProject ? "" : "studio-status-icon-unselected")} });
>
{(studio.hasRequestOutstanding ? return (
(<Spinner type="smooth" />) : <Modal
(studio.includesProject ? checkmark : plus))} className="mod-addToStudio"
contentLabel={contentLabel}
onRequestClose={this.props.onRequestClose}
isOpen={this.props.isOpen}
>
<div>
<div className="addToStudio-modal-header">
<div className="addToStudio-content-label">
{contentLabel}
</div> </div>
</div> </div>
); <div className="addToStudio-modal-content">
}); <div className="studio-list-outer-scrollbox">
<div className="studio-list-inner-scrollbox">
return ( <div className="studio-list-container">
<Modal {studioButtons}
className="mod-addToStudio" </div>
contentLabel={contentLabel} </div>
onRequestClose={this.props.onRequestClose} <div className="studio-list-bottom-gradient">
isOpen={this.props.isOpen}
>
<div>
<div className="addToStudio-modal-header">
<div className="addToStudio-content-label">
{contentLabel}
</div> </div>
</div> </div>
<div className="addToStudio-modal-content">
<div className="studio-list-outer-scrollbox">
<div className="studio-list-inner-scrollbox"> <Form
<div className="studio-list-container"> className="add-to-studio"
{studioButtons} onSubmit={this.props.onSubmit}
>
<FlexRow className="action-buttons">
<Button
className="action-button close-button white"
onClick={this.props.onRequestClose}
key="closeButton"
name="closeButton"
type="button"
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div> </div>
</div> </Button>
<div className="studio-list-bottom-gradient"> {this.props.waitingToClose ? [
</div>
</div>
<Form
className="add-to-studio"
onSubmit={this.handleSubmit}
>
<FlexRow className="action-buttons">
<Button <Button
className="action-button close-button white" className="action-button submit-button submit-button-waiting"
onClick={this.props.onRequestClose} disabled="disabled"
key="closeButton" key="submitButton"
name="closeButton" type="submit"
type="button"
> >
<div className="action-button-text"> <div className="action-button-text">
<FormattedMessage id="general.close" /> <Spinner type="smooth" />
<FormattedMessage id="addToStudio.finishing" />
</div> </div>
</Button> </Button>
{this.props.waitingToClose ? [ ] : [
<Button <Button
className="action-button submit-button submit-button-waiting" className="action-button submit-button"
disabled="disabled" key="submitButton"
key="submitButton" type="submit"
type="submit" >
> <div className="action-button-text">
<div className="action-button-text"> <FormattedMessage id="general.okay" />
<Spinner type="smooth" /> </div>
<FormattedMessage id="addToStudio.finishing" /> </Button>
</div> ]}
</Button> </FlexRow>
] : [ </Form>
<Button
className="action-button submit-button"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<FormattedMessage id="general.okay" />
</div>
</Button>
]}
</FlexRow>
</Form>
</div>
</div> </div>
</Modal> </div>
); </Modal>
} );
} }
AddToStudioModalPresentation.propTypes = { AddToStudioModalPresentation.propTypes = {

View file

@ -31,7 +31,8 @@ module.exports.getInitialState = () => ({
original: {}, original: {},
parent: {}, parent: {},
projectStudios: [], projectStudios: [],
curatedStudios: [] curatedStudios: [],
currentStudioIds: new Set()
}); });
module.exports.previewReducer = (state, action) => { module.exports.previewReducer = (state, action) => {
@ -57,49 +58,23 @@ module.exports.previewReducer = (state, action) => {
parent: action.info parent: action.info
}); });
case 'SET_PROJECT_STUDIOS': case 'SET_PROJECT_STUDIOS':
// alter the returned object so that each studio object in the array // also initialize currentStudioIds, to keep track of which studios
// includes an additional property indicating that initially, this // the project is currently in.
// studio includes the project. This is important because if it is a
// studio open to the public, which the user does not curate or own,
// and the user removes the project from that studio, we don't want
// to forget about the studio completely!
return Object.assign({}, state, { return Object.assign({}, state, {
projectStudios: action.items.map(studio => ( projectStudios: action.items,
Object.assign({}, studio, {includesProject: true}) currentStudioIds: new Set(action.items.map(studio => studio.id))
))
}); });
case 'SET_CURATED_STUDIOS': case 'SET_CURATED_STUDIOS':
return Object.assign({}, state, {curatedStudios: action.items});
case 'ADD_PROJECT_TO_STUDIO':
// add studio id to our studios-that-this-project-belongs-to set.
return Object.assign({}, state, { return Object.assign({}, state, {
curatedStudios: action.items currentStudioIds: new Set(state.currentStudioIds.add(action.studioId))
}); });
case 'ADD_TO_PROJECT_STUDIOS': case 'REMOVE_PROJECT_FROM_STUDIO':
// add studio to our studios-that-this-project-belongs-to list. state.currentStudioIds.delete(action.studioId);
// Server response doesn't include full studio object, so just use a
// minimal stub object.
return Object.assign({}, state, { return Object.assign({}, state, {
projectStudios: state.projectStudios.some(studio => ( currentStudioIds: new Set(state.currentStudioIds)
studio.id === action.studioId
)) ?
state.projectStudios.map(studio => {
if (studio.id === action.studioId) {
studio.includesProject = true;
}
return Object.assign({}, studio);
}) : state.projectStudios.concat(
{
id: action.studioId,
includesProject: true
}
)
});
case 'REMOVE_FROM_PROJECT_STUDIOS':
return Object.assign({}, state, {
projectStudios: state.projectStudios.map(studio => {
if (studio.id === action.studioId) {
studio.includesProject = false;
}
return Object.assign({}, studio);
})
}); });
case 'SET_COMMENTS': case 'SET_COMMENTS':
return Object.assign({}, state, { return Object.assign({}, state, {
@ -174,13 +149,13 @@ module.exports.setCuratedStudios = items => ({
items: items items: items
}); });
module.exports.addToProjectStudios = studioId => ({ module.exports.addProjectToStudio = studioId => ({
type: 'ADD_TO_PROJECT_STUDIOS', type: 'ADD_PROJECT_TO_STUDIO',
studioId: studioId studioId: studioId
}); });
module.exports.removeFromProjectStudios = studioId => ({ module.exports.removeProjectFromStudio = studioId => ({
type: 'REMOVE_FROM_PROJECT_STUDIOS', type: 'REMOVE_PROJECT_FROM_STUDIO',
studioId: studioId studioId: studioId
}); });
@ -465,7 +440,7 @@ module.exports.addToStudio = (studioId, projectId, token) => (dispatch => {
return; return;
} }
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED)); dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
dispatch(module.exports.addToProjectStudios(studioId)); dispatch(module.exports.addProjectToStudio(studioId));
}); });
}); });
@ -485,7 +460,7 @@ module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
return; return;
} }
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED)); dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
dispatch(module.exports.removeFromProjectStudios(studioId)); dispatch(module.exports.removeProjectFromStudio(studioId));
}); });
}); });

View file

@ -395,34 +395,28 @@ Preview.defaultProps = {
}; };
// Build consolidated curatedStudios object from all studio info. // Build consolidated curatedStudios object from all studio info.
// We add data to curatedStudios so it knows which of the studios the // We add flags to indicate whether the project is currently in the studio,
// project belongs to, and the status of requests to join/leave studios. // and the status of requests to join/leave studios.
function consolidateStudiosInfo (curatedStudios, projectStudios, studioRequests) { function consolidateStudiosInfo (curatedStudios, projectStudios,
let consolidatedStudios = []; currentStudioIds, studioRequests) {
let projectStudiosFoundInCurated = {}; // temp, for time complexity const consolidatedStudios = [];
// copy curated studios, updating any that are also in other data structures
curatedStudios.forEach(curatedStudio => {
let studioCopy = Object.assign({}, curatedStudio, {includesProject: false});
projectStudios.some(projectStudio => {
if (curatedStudio.id === projectStudio.id) {
studioCopy.includesProject = projectStudio.includesProject;
projectStudiosFoundInCurated[projectStudio.id] = true;
return true; // break out of the Array.some loop
}
});
consolidatedStudios.push(studioCopy);
});
// if there are any other studios this project is in that are NOT in
// the list of studios this user curates, like anyone-can-add-their-project
// studios, add to front of list
projectStudios.forEach(projectStudio => { projectStudios.forEach(projectStudio => {
if (!(projectStudio.id in projectStudiosFoundInCurated)) { const includesProject = currentStudioIds.has(projectStudio.id);
// no need to specify includesProject = true or false, because const consolidatedStudio =
// that state is managed by redux actions. Object.assign({}, projectStudio, {includesProject: includesProject});
consolidatedStudios.unshift(Object.assign({}, projectStudio)); consolidatedStudios.push(consolidatedStudio);
});
// copy the curated studios that project is not in
curatedStudios.forEach(curatedStudio => {
if (!currentStudioIds.has(curatedStudio.id)) {
const consolidatedStudio =
Object.assign({}, curatedStudio, {includesProject: false});
consolidatedStudios.push(consolidatedStudio);
} }
}); });
// set studio state to hasRequestOutstanding==true if it's being fetched, // set studio state to hasRequestOutstanding==true if it's being fetched,
// false if it's not // false if it's not
consolidatedStudios.forEach(consolidatedStudio => { consolidatedStudios.forEach(consolidatedStudio => {
@ -445,7 +439,8 @@ const mapStateToProps = state => ({
sessionStatus: state.session.status, sessionStatus: state.session.status,
projectStudios: state.preview.projectStudios, projectStudios: state.preview.projectStudios,
studios: consolidateStudiosInfo(state.preview.curatedStudios, studios: consolidateStudiosInfo(state.preview.curatedStudios,
state.preview.projectStudios, state.preview.status.studioRequests), state.preview.projectStudios, state.preview.currentStudioIds,
state.preview.status.studioRequests),
user: state.session.session.user, user: state.session.session.user,
playerMode: state.scratchGui.mode.isPlayerOnly, playerMode: state.scratchGui.mode.isPlayerOnly,
fullScreen: state.scratchGui.mode.isFullScreen fullScreen: state.scratchGui.mode.isFullScreen