mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 01:25:52 -05:00
spinner logic works, at least with mocked situation and no request or response from server
This commit is contained in:
parent
0a6ec304d3
commit
6e4cd63e95
3 changed files with 103 additions and 86 deletions
|
@ -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"
|
||||||
|
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue