spinner logic works, at least with mocked situation and no request or response from server

This commit is contained in:
Ben Wheeler 2018-06-20 23:12:54 -04:00
parent 0a6ec304d3
commit 6e4cd63e95
3 changed files with 103 additions and 86 deletions

View file

@ -1,12 +1,23 @@
// NOTE: next questions: // 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?
// * should we 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.
// * should the button to submit instantly? By clicking away shouldn't effectively undo what you thought you did. // * should the button to submit instantly? By clicking away shouldn't effectively undo what you thought you did.
// * should it really be pinned on the page? Isn't that something you're trying to move away from? // * should it really be pinned on the page? Isn't that something you're trying to move away from?
// * is it ok for me to make the spinner bigger and higher-radius-as-percent? (just for modal)
// *
// * // *
// plan: // plan:
// * change onOrDirty to updateQueued = {[id]: {updateType: ['join':'leave']}, ...} // * change joined to updateQueued = {[id]: {updateType: ['join':'leave']}, ...}
// * also maintain second hash, joined = {[id]: true, ...} // * also maintain second hash, joined = {[id]: true, ...}
// in render, use joined to set color, and if queued, use spinner for icon. // in render, use joined to set color, and if queued, use spinner for icon.
//
const bindAll = require('lodash.bindall'); const bindAll = require('lodash.bindall');
const truncate = require('lodash.truncate'); const truncate = require('lodash.truncate');
@ -32,7 +43,7 @@ class AddToStudioModal extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, [ // NOTE: will need to add and bind callback fn to handle addind and removing studios bindAll(this, [ // NOTE: will need to add and bind callback fn to handle addind and removing studios
'handleToggleAdded', 'handleToggle',
'handleRequestClose', 'handleRequestClose',
'handleSubmit' 'handleSubmit'
]); ]);
@ -60,33 +71,34 @@ class AddToStudioModal extends React.Component {
// prolly didn't want to be changed! // prolly didn't want to be changed!
this.state = { this.state = {
waiting: false, waitingToClose: false,
onOrDirty: {} joined: {},
updateQueued: {}
}; };
} }
componentDidMount() { componentDidMount() {
this.updateOnOrDirty(this.props.projectStudios); this.updateJoined(this.props.projectStudios);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.updateOnOrDirty(nextProps.projectStudios); this.updateJoined(nextProps.projectStudios);
} }
updateOnOrDirty(projectStudios) { updateJoined(projectStudios) {
// NOTE: in theory, myStudios could have dropped some studios in // NOTE: in theory, myStudios could have dropped some studios in
// onOrDirty, so we should check all existing onOrDirty and drop // joined, so we should check all existing joined and drop
// them too; otherwise we might retain a dirty change for a studio // them too; otherwise we might retain a dirty change for a studio
// we no longer have permission for. In theory. // we no longer have permission for. In theory.
let onOrDirty = Object.assign({}, this.state.onOrDirty); let joined = Object.assign({}, this.state.joined);
projectStudios.forEach((studio) => { projectStudios.forEach((studio) => {
onOrDirty[studio.id] = {added: true, dirty: false}; joined[studio.id] = true;
}); });
console.log(projectStudios); console.log(projectStudios);
console.log(onOrDirty); console.log(joined);
if (!this.deepCompare(onOrDirty, this.state.onOrDirty)) { if (!this.deepCompare(joined, this.state.joined)) {
this.setState({onOrDirty: Object.assign({}, onOrDirty)}); this.setState({joined: Object.assign({}, joined)});
} }
} }
@ -114,79 +126,58 @@ class AddToStudioModal extends React.Component {
return true; return true;
}; };
handleToggleAdded(studioId) { handleToggle(studioId) {
let onOrDirty = this.state.onOrDirty; const joined = this.state.joined;
if (studioId in onOrDirty) { let updateQueued = this.state.updateQueued;
if (onOrDirty[studioId].added === true) { if (studioId in joined) { // so we want to leave the studio...
if (onOrDirty[studioId].dirty === true) { if (studioId in updateQueued) {
// let's untrack the status of this studio, so it's // we've already requested it... should we request again??
// un-added, and un-dirty again } else { // need to request it...
delete onOrDirty[studioId]; console.log("queueing leave request from studio: " + studioId);
} else { // it started off added, so it's dirty now updateQueued[studioId] = {updateType: 'leave'};
onOrDirty[studioId].added = false; // NOTE: this should work with regular updateQueued, not object.assign, right?
onOrDirty[studioId].dirty = true; // test it...
} this.setState({updateQueued: Object.assign({}, updateQueued)});
} else { }
if (onOrDirty[studioId].dirty === true) { } else { // we want to join
// it was previously set to unadded. so let's set it to if (studioId in updateQueued) {
// added, and NOT dirty. This is how it started out // we've already requested it... should we request again??
onOrDirty[studioId].added = true; } else { // need to request it...
onOrDirty[studioId].dirty = false; console.log("queueing join request to studio: " + studioId);
} updateQueued[studioId] = {updateType: 'join'};
// should never be added == false AND dirty == false this.setState({updateQueued: Object.assign({}, updateQueued)});
} }
} else { // was not in onOrDirty; add it as added!
onOrDirty[studioId] = {added: true, dirty: true};
} }
this.setState({onOrDirty: Object.assign({}, onOrDirty)}); }
// 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 () { handleRequestClose () {
// NOTE that we do NOT clear onOrDirty, so we don't lose // NOTE that we do NOT clear joined, so we don't lose
// user's work from a stray click outside the modal... // user's work from a stray click outside the modal...
// but maybe this should be different? // but maybe this should be different?
this.baseModal.handleRequestClose(); this.baseModal.handleRequestClose();
} }
handleSubmit (formData) { handleSubmit (formData) {
// NOTE: ignoring formData for now... // NOTE:For this approach to work, we need to separately handle
this.setState({waiting: true}, () => { // server responses to our update requests,
const onOrDirty = this.state.onOrDirty; // and after each one, check to see if there are no outstanding updates
const studiosToAdd = Object.keys(onOrDirty) // queued.
.reduce(function(accumulator, key) { this.setState({waitingToClose: true}, () => {
if (onOrDirty[key].dirty === true && this.checkForOutstandingUpdates();
onOrDirty[key].added === true) {
accumulator.push(key);
}
return accumulator;
}, []);
const studiosToLeave = Object.keys(onOrDirty)
.reduce(function(accumulator, key) {
if (onOrDirty[key].dirty === true &&
onOrDirty[key].added === false) {
accumulator.push(key);
}
return accumulator;
}, []);
setTimeout(function() {
this.props.onAddToStudio(studiosToAdd, studiosToLeave, err => {
if (err) log.error(err);
// When this modal is opened, and isOpen becomes true,
// onOrDirty should start with a clean slate
// NOTE: this doesn't seem to be working:
setTimeout(function() {
this.setState({
waiting: false,
onOrDirty: {}
});
}.bind(this), 3000);
// this.setState({
// waiting: false,
// onOrDirty: {}
// });
});
}.bind(this), 3000);
}); });
} }
render () { render () {
@ -198,16 +189,17 @@ class AddToStudioModal extends React.Component {
type, type,
...modalProps ...modalProps
} = this.props; } = this.props;
const onOrDirty = this.state.onOrDirty; const joined = this.state.joined;
const updateQueued = this.state.updateQueued;
const contentLabel = intl.formatMessage({id: `addToStudio.${type}`}); const contentLabel = intl.formatMessage({id: `addToStudio.${type}`});
const studioButtons = myStudios.map((studio, index) => { const studioButtons = myStudios.map((studio, index) => {
const isAdded = (studio.id in onOrDirty && const isAdded = (studio.id in joined);
onOrDirty[studio.id].added === true); const isWaiting = (studio.id in updateQueued);
return ( return (
<div className={"studio-selector-button" + <div className={"studio-selector-button" +
(isAdded ? " studio-selector-button-selected" : "")} (isAdded ? " studio-selector-button-selected" : "")}
key={studio.id} key={studio.id}
onClick={() => this.handleToggleAdded(studio.id)} onClick={() => this.handleToggle(studio.id)}
> >
<div className="studio-selector-button-text"> <div className="studio-selector-button-text">
{truncate(studio.title, {'length': 20, 'separator': /[,:\.;]*\s+/})} {truncate(studio.title, {'length': 20, 'separator': /[,:\.;]*\s+/})}
@ -215,7 +207,7 @@ class AddToStudioModal extends React.Component {
<div className={"studio-status-icon" + <div className={"studio-status-icon" +
(isAdded ? " studio-status-icon-selected" : "")} (isAdded ? " studio-status-icon-selected" : "")}
> >
{isAdded ? "✓" : "+"} {isWaiting ? (<Spinner />) : (isAdded ? "✓" : "+")}
</div> </div>
</div> </div>
); );
@ -264,7 +256,7 @@ class AddToStudioModal extends React.Component {
<FormattedMessage id="general.close" /> <FormattedMessage id="general.close" />
</div> </div>
</Button> </Button>
{this.state.waiting ? [ {this.state.waitingToClose ? [
<Button <Button
className="action-button submit-button" className="action-button submit-button"
disabled="disabled" disabled="disabled"

View file

@ -144,3 +144,28 @@
.submit-button { .submit-button {
background-color: #3F9AFB; background-color: #3F9AFB;
} }
.action-button-text .spinner {
margin: -0.125rem auto 2rem;
height: 30px;
width: 1.875rem;
}
.studio-status-icon .spinner {
position: unset; /* don't understand why neither relative nor absolute work */
}
.studio-status-icon .spinner .circle {
/* overlay spinner on circle */
position: absolute;
width: 75%; /* stay within boundaries of circle */
height: 75%; /* stay within boundaries of circle */
margin: 0.1875rem; /* stay within boundaries of circle */
}
/* for both .studio-status-icon and .action-button-text : */
.spinner .circle:before {
/* make spinner come closer to center of circle; was too hard to see */
height: 30%;
}

View file

@ -24,7 +24,7 @@
content: ""; content: "";
.white & { .white & {
background-color: darken($ui-blue, 8%); background-color: darken($ui-blue, 8%);
} }
} }
} }
@ -37,7 +37,7 @@
transform: rotate($rotation); transform: rotate($rotation);
&:before { &:before {
animation-delay: $delay; animation-delay: $delay;
} }
} }
@ -54,5 +54,5 @@
40% { 40% {
opacity: 1; opacity: 1;
} }
} }