addtostudiomodal: incomplete progress on 1. container-presentation refactor, 2. redux handling of add and leave studio requests

This commit is contained in:
Ben Wheeler 2018-07-11 15:08:01 -04:00
parent 9cac70ab18
commit 6a32edb2fe
5 changed files with 168 additions and 257 deletions

View file

@ -42,6 +42,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;
require('../../forms/button.scss');
require('./modal.scss');
@ -61,61 +63,59 @@ class AddToStudioModal extends React.Component {
// membership/stats/name. use that for rendering.
this.state = {
waitingToClose: false,
joined: {},
updateQueued: {}
studioState: {},
};
}
componentDidMount() {
this.updateJoined(this.props.projectStudios);
this.updateStudioState(this.props.projectStudios, this.props.curatedStudios);
}
componentWillReceiveProps(nextProps) {
this.updateJoined(nextProps.projectStudios);
this.updateStudioState(nextProps.projectStudios, nextProps.curatedStudios);
}
updateJoined(projectStudios) {
// projectStudios could have dropped some studios since the last time
// we traveresd it, so we should build the joined state object
// from scratch.
updateStudioState(projectStudios, curatedStudios) {
// can't just use the spread operator here, because we may have
// project studios removed from the list.
let joined = Object.assign({}, this.state.joined);
projectStudios.forEach((studio) => {
joined[studio.id] = true;
});
this.setState({joined: Object.assign({}, joined)});
}
// NOTE: This isn't handling the removal of a studio from the list well.
requestJoinStudio(studioId) {
// submit here? or through presentation?
}
requestLeaveStudio(studioId) {
// submit here? or through presentation?
// 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) {
const joined = this.state.joined;
const updateQueued = this.state.updateQueued;
console.log(updateQueued)
if (!(studioId in updateQueued)) { // we haven't requested it yet...
const updateType = (studioId in joined) ? 'leave' : 'join';
console.log("queueing " + updateType + " request for studio: " + studioId);
this.setState(prevState => ({
updateQueued: {
...prevState.updateQueued,
[studioId]: {updateType: updateType}
}
}), () => { // callback
// submit request to server
if (updateType === 'join') {
this.requestJoinStudio(studioId);
} else {
this.requestLeaveStudio(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
}
}
@ -151,132 +151,26 @@ class AddToStudioModal extends React.Component {
}
render () {
const {
intl,
projectStudios,
curatedStudios,
onAddToStudio, // eslint-disable-line no-unused-vars
isOpen,
onRequestClose
} = this.props;
const joined = this.state.joined;
const updateQueued = this.state.updateQueued;
const contentLabel = intl.formatMessage({id: "addToStudio.title"});
const checkmark = <img alt="checkmark-icon"
className="studio-status-icon-checkmark-img"
src="/svgs/modal/confirm.svg"
/>
const plus = <img alt="plus-icon"
className="studio-status-icon-plus-img"
src="/svgs/modal/add.svg"
/>
const studioButtons = curatedStudios.map((studio, index) => {
const isAdded = (studio.id in joined);
const isWaiting = (studio.id in updateQueued);
return (
<div className={"studio-selector-button " +
(isWaiting ? "studio-selector-button-waiting" :
(isAdded ? "studio-selector-button-selected" : ""))}
key={studio.id}
onClick={() => this.handleToggle(studio.id)}
>
<div className={"studio-selector-button-text " +
((isWaiting || isAdded) ? "studio-selector-button-text-selected" :
".studio-selector-button-text-unselected")}>
{truncate(studio.title, {'length': 20, 'separator': /[,:\.;]*\s+/})}
</div>
<div className={"studio-status-icon" +
((isWaiting || isAdded) ? "" : " studio-status-icon-unselected")}
>
{isWaiting ? (<Spinner type="smooth" />) : (isAdded ? checkmark : plus)}
</div>
</div>
);
});
return (
<Modal
className="mod-addToStudio"
contentLabel={contentLabel}
ref={component => { // bind to base modal, to pass handleRequestClose through
this.baseModal = component;
}}
onRequestClose={onRequestClose}
isOpen={isOpen}
>
<div>
<div className="addToStudio-modal-header">
<div className="addToStudio-content-label">
{contentLabel}
</div>
</div>
<div className="addToStudio-modal-content">
<div className="studio-list-outer-scrollbox">
<div className="studio-list-inner-scrollbox">
<div className="studio-list-container">
{studioButtons}
</div>
</div>
<div className="studio-list-bottom-gradient">
</div>
</div>
<Form
className="add-to-studio"
onSubmit={this.handleSubmit}
>
<FlexRow className="action-buttons">
<Button
className="action-button close-button white"
onClick={this.handleRequestClose}
key="closeButton"
name="closeButton"
type="button"
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
</Button>
{this.state.waitingToClose ? [
<Button
className="action-button submit-button submit-button-waiting"
disabled="disabled"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<Spinner type="smooth" />
<FormattedMessage id="addToStudio.finishing" />
</div>
</Button>
] : [
<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>
</Modal>
<AddToStudioModalPresentation
studios={curatedStudios}
studioState={this.state.studioState}
isOpen={isOpen}
onToggleStudio={handleToggle}
/>
);
}
}
AddToStudioModal.propTypes = {
intl: intlShape,
projectStudios: PropTypes.arrayOf(PropTypes.object),
curatedStudios: PropTypes.arrayOf(PropTypes.object),
onAddToStudio: PropTypes.func,
onRequestClose: PropTypes.func,
type: PropTypes.string
studioRequests: PropTypes.object,
onToggleStudio: PropTypes.func,
onRequestClose: PropTypes.func
};
module.exports = injectIntl(AddToStudioModal);

View file

@ -1,3 +1,4 @@
const keyMirror = require('keymirror');
const bindAll = require('lodash.bindall');
const truncate = require('lodash.truncate');
const PropTypes = require('prop-types');
@ -18,11 +19,17 @@ 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
'handleToggle',
'handleRequestClose',
'handleSubmit'
]);
@ -33,83 +40,25 @@ class AddToStudioModalPresentation extends React.Component {
// membership/stats/name. use that for rendering.
this.state = {
waitingToClose: false,
joined: {},
updateQueued: {}
studios: props.studios
};
}
requestJoinStudio(studioId) {
// submit here? or through presentation?
}
requestLeaveStudio(studioId) {
// submit here? or through presentation?
}
handleToggle(studioId) {
const joined = this.state.joined;
const updateQueued = this.state.updateQueued;
console.log(updateQueued)
if (!(studioId in updateQueued)) { // we haven't requested it yet...
const updateType = (studioId in joined) ? 'leave' : 'join';
console.log("queueing " + updateType + " request for studio: " + studioId);
this.setState(prevState => ({
updateQueued: {
...prevState.updateQueued,
[studioId]: {updateType: updateType}
}
}), () => { // callback
// submit request to server
if (updateType === 'join') {
this.requestJoinStudio(studioId);
} else {
this.requestLeaveStudio(studioId);
}
});
}
}
// 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.props.handleSubmit(formData);
}
render () {
// NOTE: how does intl get injected?
const {
intl,
studios,
onAddToStudio, // eslint-disable-line no-unused-vars
isOpen,
onRequestClose
onToggleStudio,
isOpen
} = this.props;
const joined = this.state.joined;
const updateQueued = this.state.updateQueued;
const contentLabel = intl.formatMessage({id: "addToStudio.title");
const contentLabel = intl.formatMessage({id: "addToStudio.title"});
const checkmark = <img alt="checkmark-icon"
className="studio-status-icon-checkmark-img"
src="/svgs/modal/confirm.svg"
@ -118,25 +67,30 @@ class AddToStudioModalPresentation extends React.Component {
className="studio-status-icon-plus-img"
src="/svgs/modal/add.svg"
/>
const studioButtons = curatedStudios.map((studio, index) => {
const isAdded = (studio.id in joined);
const isWaiting = (studio.id in updateQueued);
const studioButtons = studios.map((studio, index) => {
const thisStudioState = studioState[studio.id];
return (
<div className={"studio-selector-button " +
(isWaiting ? "studio-selector-button-waiting" :
(isAdded ? "studio-selector-button-selected" : ""))}
(thisStudioState === module.exports.State.JOINING ||
thisStudioState === module.exports.State.LEAVING) ?
"studio-selector-button-waiting" :
(thisStudioState === module.exports.State.IN ? "studio-selector-button-selected" : ""))}
key={studio.id}
onClick={() => this.handleToggle(studio.id)}
onClick={() => this.props.onToggleStudio(studio.id)}
>
<div className={"studio-selector-button-text " +
((isWaiting || isAdded) ? "studio-selector-button-text-selected" :
".studio-selector-button-text-unselected")}>
(thisStudioState === module.exports.State.OUT ?
"studio-selector-button-text-unselected" :
".studio-selector-button-text-selected")}>
{truncate(studio.title, {'length': 20, 'separator': /[,:\.;]*\s+/})}
</div>
<div className={"studio-status-icon" +
((isWaiting || isAdded) ? "" : " studio-status-icon-unselected")}
<div className={"studio-status-icon " +
(thisStudioState === module.exports.State.OUT ? "studio-status-icon-unselected" : "")}
>
{isWaiting ? (<Spinner type="smooth" />) : (isAdded ? checkmark : plus)}
{(thisStudioState === module.exports.State.JOINING ||
thisStudioState === module.exports.State.LEAVING) ?
(<Spinner type="smooth" />) :
(thisStudioState === module.exports.State.IN ? checkmark : plus)}
</div>
</div>
);
@ -149,7 +103,7 @@ class AddToStudioModalPresentation extends React.Component {
ref={component => { // bind to base modal, to pass handleRequestClose through
this.baseModal = component;
}}
onRequestClose={onRequestClose}
onRequestClose={this.props.onRequestClose}
isOpen={isOpen}
>
<div>
@ -188,13 +142,14 @@ class AddToStudioModalPresentation extends React.Component {
</Button>
{this.state.waitingToClose ? [
<Button
className="action-button submit-button"
className="action-button submit-button submit-button-waiting"
disabled="disabled"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<Spinner />
<Spinner type="smooth" />
<FormattedMessage id="addToStudio.finishing" />
</div>
</Button>
] : [
@ -225,4 +180,4 @@ AddToStudioModalPresentation.propTypes = {
onRequestClose: PropTypes.func
};
module.exports = injectIntl(AddToStudioModal);
module.exports = injectIntl(AddToStudioModalPresentation);

View file

@ -20,7 +20,8 @@ module.exports.getInitialState = () => ({
parent: module.exports.Status.NOT_FETCHED,
remixes: module.exports.Status.NOT_FETCHED,
projectStudios: module.exports.Status.NOT_FETCHED,
curatedStudios: module.exports.Status.NOT_FETCHED
curatedStudios: module.exports.Status.NOT_FETCHED,
studioRequests: {}
},
projectInfo: {},
remixes: [],
@ -30,7 +31,7 @@ module.exports.getInitialState = () => ({
original: {},
parent: {},
projectStudios: [],
curatedStudios: []
curatedStudios: [],
});
module.exports.previewReducer = (state, action) => {
@ -79,6 +80,10 @@ module.exports.previewReducer = (state, action) => {
state = JSON.parse(JSON.stringify(state));
state.status[action.infoType] = action.status;
return state;
case 'SET_STUDIO_FETCH_STATUS':
state = JSON.parse(JSON.stringify(state));
state.status.studioRequests[action.studioId] = action.status;
return state;
case 'ERROR':
log.error(action.error);
return state;
@ -132,12 +137,20 @@ 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.setFetchStatus = (type, status) => ({
type: 'SET_FETCH_STATUS',
infoType: type,
status: status
});
module.exports.setStudioFetchStatus = (studioId, status) => ({
type: 'SET_STUDIO_FETCH_STATUS',
studioId: studioId,
status: status
});
module.exports.getProjectInfo = (id, token) => (dispatch => {
const opts = {
uri: `/projects/${id}`
@ -391,6 +404,44 @@ module.exports.getCuratedStudios = (username, token) => (dispatch => {
});
});
module.exports.addToStudio = (studioId, projectId, token) => (dispatch => {
api({
uri: `/studios/${studioId}/project/${projectId}`,
authentication: token,
method: 'POST'
}, (err, body) => {
if (err) {
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setError('Add to studio returned no data'));
return;
}
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
// NOTE: is there a way here to update or refresh the project studio list?
});
});
module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
api({
uri: `/studios/${studioId}/project/${projectId}`,
authentication: token,
method: 'DELETE'
}, (err, body) => {
if (err) {
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined') {
dispatch(module.exports.setError('Leave studio returned no data'));
return;
}
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
// NOTE: is there a way here to update or refresh the project studio list?
});
});
module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
api({
uri: `/projects/${id}`,

View file

@ -33,7 +33,6 @@ class PreviewPresentation extends React.Component {
bindAll(this, [
'handleAddToStudioClick',
'handleAddToStudioClose',
'handleAddToStudioSubmit',
'handleReportClick',
'handleReportClose',
'handleReportSubmit'
@ -184,24 +183,6 @@ class PreviewPresentation extends React.Component {
handleAddToStudioClose () {
this.setState({addToStudioOpen: false});
}
handleAddToStudioSubmit (studiosToAdd, studiosToLeave, callback) {
console.log('studios to add: ');
console.log(studiosToAdd);
console.log('studios to leave: ');
console.log(studiosToLeave);
// const data = {
// ...formData,
// id: this.props.projectId,
// username: this.props.user.username
// };
//console.log('submit addToStudio data', data); // eslint-disable-line no-console
// TODO: post to API; in that callback,
// pass error to modal via its callback.
this.setState({addToStudioOpen: false}, () => {
callback();
});
}
// Report Project modal
handleReportClick (e) {
@ -240,6 +221,8 @@ class PreviewPresentation extends React.Component {
sessionStatus,
projectStudios,
curatedStudios,
studioRequests,
onToggleStudio,
user,
onFavoriteClicked,
onLoveClicked,
@ -493,7 +476,8 @@ class PreviewPresentation extends React.Component {
key="add-to-studio-modal"
projectStudios={projectStudios}
curatedStudios={this.mockedMyStudios}
onAddToStudio={this.handleAddToStudioSubmit}
studioRequests={studioRequests}
onToggleStudio={onToggleStudio}
onRequestClose={this.handleAddToStudioClose}
/>
]
@ -608,6 +592,8 @@ 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,
banned: PropTypes.bool,

View file

@ -126,6 +126,21 @@ 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
);
}
}
handleFavoriteToggle () {
this.props.setFavedStatus(
!this.props.faved,
@ -203,6 +218,8 @@ 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}
onLoveClicked={this.handleLoveToggle}
@ -245,6 +262,7 @@ 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,
@ -274,6 +292,7 @@ const mapStateToProps = state => ({
sessionStatus: state.session.status,
projectStudios: state.preview.projectStudios,
curatedStudios: state.preview.curatedStudios,
studioRequests: state.preview.status.studioRequests,
user: state.session.session.user,
playerMode: state.scratchGui.mode.isPlayerOnly,
fullScreen: state.scratchGui.mode.isFullScreen
@ -299,6 +318,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));
},
getFavedStatus: (id, username, token) => {
dispatch(previewActions.getFavedStatus(id, username, token));
},