mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-02-17 08:31:23 -05:00
addtostudio modal interfaces with api add/remove endpoints; waits to close; handles open studios that user does not curate.
addtostudio modal interfaces with api add/remove endpoints; waits to close; handles open studios that user does not curate.
This commit is contained in:
parent
a59d533a1c
commit
fc5912526e
9 changed files with 130 additions and 113 deletions
|
@ -1,38 +1,15 @@
|
|||
// sample data:
|
||||
// this.studios = [{name: 'Funny games', id: 1}, {name: 'Silly ideas', id: 2}];
|
||||
// studios data is like:
|
||||
// [{
|
||||
// id: 1702295,
|
||||
// description: "...",
|
||||
// history: {created: "2015-11-15T00:24:35.000Z",
|
||||
// modified: "2018-05-01T00:14:48.000Z"},
|
||||
// image: "http....png",
|
||||
// owner: 10689298,
|
||||
// stats: {followers: 0},
|
||||
// title: "Studio title"
|
||||
// }, {...}]
|
||||
const bindAll = require('lodash.bindall');
|
||||
const truncate = require('lodash.truncate');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const log = require('../../../lib/log.js');
|
||||
|
||||
const Form = require('../../forms/form.jsx');
|
||||
const Button = require('../../forms/button.jsx');
|
||||
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 previewActions = require('../../../redux/preview.js');
|
||||
|
||||
require('../../forms/button.scss');
|
||||
require('./modal.scss');
|
||||
|
||||
class AddToStudioModal extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'closeAndStopWaiting',
|
||||
'handleSubmit'
|
||||
]);
|
||||
|
||||
|
@ -42,21 +19,27 @@ class AddToStudioModal extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUpdate (prevProps) {
|
||||
checkIfFinishedUpdating();
|
||||
this.checkIfFinishedUpdating();
|
||||
}
|
||||
|
||||
hasOutstandingUpdates () {
|
||||
return (studios.some(studio => (studio.hasRequestOutstanding === true)));
|
||||
return (this.props.studios.some(studio => (studio.hasRequestOutstanding === true)));
|
||||
}
|
||||
|
||||
checkIfFinishedUpdating () {
|
||||
if (waitingToClose === true && hasOutstandingUpdates() === false) {
|
||||
this.setState({waitingToClose: false}, () => {
|
||||
this.props.onRequestClose();
|
||||
});
|
||||
if (this.state.waitingToClose === true && this.hasOutstandingUpdates() === false) {
|
||||
this.closeAndStopWaiting();
|
||||
}
|
||||
}
|
||||
|
||||
// need to register here that we are no longer waiting for the modal to close.
|
||||
// Otherwise, user may reopen modal only to have it immediately close.
|
||||
closeAndStopWaiting () {
|
||||
this.setState({waitingToClose: false}, () => {
|
||||
this.props.onRequestClose();
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit (formData) {
|
||||
this.setState({waitingToClose: true}, () => {
|
||||
this.checkIfFinishedUpdating();
|
||||
|
@ -64,19 +47,14 @@ class AddToStudioModal extends React.Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
isOpen,
|
||||
studios,
|
||||
onToggleStudio,
|
||||
onRequestClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AddToStudioModalPresentation
|
||||
studios={studios}
|
||||
isOpen={isOpen}
|
||||
onToggleStudio={onToggleStudio}
|
||||
onSubmit={handleSubmit}
|
||||
studios={this.props.studios}
|
||||
isOpen={this.props.isOpen}
|
||||
waitingToClose={this.state.waitingToClose}
|
||||
onRequestClose={this.closeAndStopWaiting}
|
||||
onToggleStudio={this.props.onToggleStudio}
|
||||
onSubmit={this.handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -89,4 +67,4 @@ AddToStudioModal.propTypes = {
|
|||
onRequestClose: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = AddToStudioModalPresentation;
|
||||
module.exports = AddToStudioModal;
|
||||
|
|
|
@ -12,7 +12,6 @@ const Form = require('../../forms/form.jsx');
|
|||
const Button = require('../../forms/button.jsx');
|
||||
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');
|
||||
|
||||
require('../../forms/button.scss');
|
||||
|
@ -24,11 +23,6 @@ class AddToStudioModalPresentation extends React.Component {
|
|||
bindAll(this, [
|
||||
'handleSubmit'
|
||||
]);
|
||||
|
||||
this.state = {
|
||||
waitingToClose: false,
|
||||
studios: props.studios
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit (formData) {
|
||||
|
@ -36,13 +30,7 @@ class AddToStudioModalPresentation extends React.Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
intl,
|
||||
studios,
|
||||
onToggleStudio,
|
||||
isOpen
|
||||
} = this.props;
|
||||
const contentLabel = intl.formatMessage({id: "addToStudio.title"});
|
||||
const contentLabel = this.props.intl.formatMessage({id: "addToStudio.title"});
|
||||
const checkmark = <img alt="checkmark-icon"
|
||||
className="studio-status-icon-checkmark-img"
|
||||
src="/svgs/modal/confirm.svg"
|
||||
|
@ -51,7 +39,7 @@ class AddToStudioModalPresentation extends React.Component {
|
|||
className="studio-status-icon-plus-img"
|
||||
src="/svgs/modal/add.svg"
|
||||
/>
|
||||
const studioButtons = studios.map((studio, index) => {
|
||||
const studioButtons = this.props.studios.map((studio, index) => {
|
||||
return (
|
||||
<div className={"studio-selector-button " +
|
||||
(studio.hasRequestOutstanding ? "studio-selector-button-waiting" :
|
||||
|
@ -80,7 +68,7 @@ class AddToStudioModalPresentation extends React.Component {
|
|||
className="mod-addToStudio"
|
||||
contentLabel={contentLabel}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
isOpen={isOpen}
|
||||
isOpen={this.props.isOpen}
|
||||
>
|
||||
<div>
|
||||
<div className="addToStudio-modal-header">
|
||||
|
@ -116,7 +104,7 @@ class AddToStudioModalPresentation extends React.Component {
|
|||
<FormattedMessage id="general.close" />
|
||||
</div>
|
||||
</Button>
|
||||
{this.state.waitingToClose ? [
|
||||
{this.props.waitingToClose ? [
|
||||
<Button
|
||||
className="action-button submit-button submit-button-waiting"
|
||||
disabled="disabled"
|
||||
|
@ -143,7 +131,6 @@ class AddToStudioModalPresentation extends React.Component {
|
|||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -151,8 +138,10 @@ class AddToStudioModalPresentation extends React.Component {
|
|||
|
||||
AddToStudioModalPresentation.propTypes = {
|
||||
intl: intlShape,
|
||||
isOpen: PropTypes.bool,
|
||||
studios: PropTypes.arrayOf(PropTypes.object),
|
||||
onAddToStudio: PropTypes.func,
|
||||
waitingToClose: PropTypes.bool,
|
||||
onToggleStudio: PropTypes.func,
|
||||
onRequestClose: PropTypes.func,
|
||||
onSubmit: PropTypes.func
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
}
|
||||
|
||||
.report-modal-header {
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
|
||||
background-color: $ui-coral;
|
||||
padding-top: .75rem;
|
||||
|
@ -36,7 +36,7 @@
|
|||
width: 80%;
|
||||
line-height: 1.5rem;
|
||||
font-size: .875rem;
|
||||
|
||||
|
||||
.validation-message {
|
||||
$arrow-border-width: 1rem;
|
||||
display: block;
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
&:before {
|
||||
display: block;
|
||||
animation: circleFadeDelaySmooth 1.2s infinite ease-in-out both;
|
||||
animation: circleFadeDelaySmooth 1.8s infinite ease-in-out both;
|
||||
margin: 0 auto;
|
||||
border-radius: 100%;
|
||||
background-color: $ui-white;
|
||||
|
@ -93,7 +93,7 @@
|
|||
|
||||
@for $i from 1 through 24 {
|
||||
$rotation: 15deg * ($i - 1);
|
||||
$delay: -1.3s + $i * .05;
|
||||
$delay: -1.9s + $i * .075;
|
||||
|
||||
.circle#{$i} {
|
||||
transform: rotate($rotation);
|
||||
|
|
|
@ -162,8 +162,6 @@
|
|||
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
|
||||
|
||||
"thumbnail.by": "by",
|
||||
"addToStudio.title": "Add to Studio",
|
||||
"addToStudio.finishing": "Finishing up...",
|
||||
"report.project": "Report Project",
|
||||
"report.projectInstructions": "From the dropdown below, please select the reason why you feel this project is disrespectful or inappropriate or otherwise breaks the {CommunityGuidelinesLink}.",
|
||||
"report.CommunityGuidelinesLinkText": "Scratch Community Guidelines",
|
||||
|
|
|
@ -57,23 +57,49 @@ module.exports.previewReducer = (state, action) => {
|
|||
parent: action.info
|
||||
});
|
||||
case 'SET_PROJECT_STUDIOS':
|
||||
// alter the returned object so that each studio object in the array
|
||||
// includes an additional property indicating that initially, this
|
||||
// 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, {
|
||||
projectStudios: action.items
|
||||
projectStudios: action.items.map(studio => (
|
||||
Object.assign({}, studio, {includesProject: true})
|
||||
))
|
||||
});
|
||||
case 'SET_CURATED_STUDIOS':
|
||||
return Object.assign({}, state, {
|
||||
curatedStudios: action.items
|
||||
});
|
||||
case 'ADD_TO_PROJECT_STUDIOS':
|
||||
// add studio to our studios-that-this-project-belongs-to list.
|
||||
// Server response doesn't include full studio object, so just use a
|
||||
// minimal stub object.
|
||||
return Object.assign({}, state, {
|
||||
// NOTE: move this to calling fn, make this add object passed to me
|
||||
projectStudios: state.projectStudios.concat({id: action.studioId})
|
||||
projectStudios: state.projectStudios.some(studio => (
|
||||
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.filter(studio => (
|
||||
studio.id !== action.studioId
|
||||
))
|
||||
projectStudios: state.projectStudios.map(studio => {
|
||||
if (studio.id === action.studioId) {
|
||||
studio.includesProject = false;
|
||||
}
|
||||
return Object.assign({}, studio);
|
||||
})
|
||||
});
|
||||
case 'SET_COMMENTS':
|
||||
return Object.assign({}, state, {
|
||||
|
@ -402,7 +428,7 @@ module.exports.getProjectStudios = id => (dispatch => {
|
|||
module.exports.getCuratedStudios = (username, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `/user/${username}/studios/curate`,
|
||||
uri: `/users/${username}/studios/curate`,
|
||||
authentication: token
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
|
@ -439,9 +465,6 @@ module.exports.addToStudio = (studioId, projectId, token) => (dispatch => {
|
|||
return;
|
||||
}
|
||||
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
|
||||
// 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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"addToStudio.title": "Add to Studio",
|
||||
"addToStudio.finishing": "Finishing up...",
|
||||
"preview.musicExtensionChip": "Music",
|
||||
"preview.penExtensionChip": "Pen",
|
||||
"preview.speechExtensionChip": "Google Speech",
|
||||
"preview.translateExtensionChip": "Google Translate",
|
||||
"preview.videoMotionChip": "Video Motion"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,14 +42,17 @@ const PreviewPresentation = ({
|
|||
projectInfo,
|
||||
remixes,
|
||||
report,
|
||||
addToStudioOpen,
|
||||
projectStudios,
|
||||
curatedStudios,
|
||||
studios,
|
||||
userOwnsProject,
|
||||
onFavoriteClicked,
|
||||
onLoveClicked,
|
||||
onReportClicked,
|
||||
onReportClose,
|
||||
onReportSubmit,
|
||||
onAddToStudioClicked,
|
||||
onAddToStudioClosed,
|
||||
onToggleStudio,
|
||||
onSeeInside,
|
||||
onUpdate
|
||||
|
@ -244,16 +247,16 @@ const PreviewPresentation = ({
|
|||
<React.Fragment>
|
||||
<Button className="action-button studio-button"
|
||||
key="add-to-studio-button"
|
||||
onClick={this.handleAddToStudioClick}
|
||||
onClick={onAddToStudioClicked}
|
||||
>
|
||||
Add to Studio
|
||||
</Button>,
|
||||
<AddToStudioModal
|
||||
isOpen={this.state.addToStudioOpen}
|
||||
isOpen={addToStudioOpen}
|
||||
key="add-to-studio-modal"
|
||||
studios={curatedStudios}
|
||||
studios={studios}
|
||||
onToggleStudio={onToggleStudio}
|
||||
onRequestClose={this.handleAddToStudioClose}
|
||||
onRequestClose={onAddToStudioClosed}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
@ -338,8 +341,11 @@ PreviewPresentation.propTypes = {
|
|||
open: PropTypes.bool,
|
||||
waiting: PropTypes.bool
|
||||
}),
|
||||
addToStudioOpen: PropTypes.bool,
|
||||
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
curatedStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
studios: PropTypes.arrayOf(PropTypes.object),
|
||||
onAddToStudioClicked: PropTypes.func,
|
||||
onAddToStudioClosed: PropTypes.func,
|
||||
onToggleStudio: PropTypes.func,
|
||||
userOwnsProject: PropTypes.bool
|
||||
};
|
||||
|
|
|
@ -35,6 +35,8 @@ class Preview extends React.Component {
|
|||
'handleReportClick',
|
||||
'handleReportClose',
|
||||
'handleReportSubmit',
|
||||
'handleAddToStudioClick',
|
||||
'handleAddToStudioClose',
|
||||
'handleSeeInside',
|
||||
'handleUpdate',
|
||||
'initCounts',
|
||||
|
@ -53,6 +55,7 @@ class Preview extends React.Component {
|
|||
favoriteCount: 0,
|
||||
loveCount: 0,
|
||||
projectId: parts[1] === 'editor' ? 0 : parts[1],
|
||||
addToStudioOpen: false,
|
||||
report: {
|
||||
category: '',
|
||||
notes: '',
|
||||
|
@ -147,6 +150,13 @@ class Preview extends React.Component {
|
|||
handleReportClose () {
|
||||
this.setState({report: {...this.state.report, open: false}});
|
||||
}
|
||||
handleAddToStudioClick () {
|
||||
this.setState({addToStudioOpen: true});
|
||||
}
|
||||
handleAddToStudioClose () {
|
||||
this.setState({addToStudioOpen: false});
|
||||
}
|
||||
// NOTE: this is a copy, change it
|
||||
handleReportSubmit (formData) {
|
||||
this.setState({report: {
|
||||
category: formData.report_category,
|
||||
|
@ -201,13 +211,17 @@ class Preview extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
handleToggleStudio (studioId, isAdd) {
|
||||
this.props.toggleStudio(
|
||||
isAdd,
|
||||
studioId,
|
||||
this.props.projectInfo.id,
|
||||
this.props.user.token
|
||||
);
|
||||
handleToggleStudio (studioId) {
|
||||
const studio = this.props.studios.find((studio) => {return studio.id === studioId});
|
||||
// only send add or leave request to server if we know current status
|
||||
if (studio !== undefined && ('includesProject' in studio)) {
|
||||
this.props.toggleStudio(
|
||||
(studio.includesProject === false),
|
||||
studioId,
|
||||
this.props.projectInfo.id,
|
||||
this.props.user.token
|
||||
);
|
||||
}
|
||||
}
|
||||
handleFavoriteToggle () {
|
||||
this.props.setFavedStatus(
|
||||
|
@ -309,9 +323,9 @@ class Preview extends React.Component {
|
|||
projectInfo={this.props.projectInfo}
|
||||
remixes={this.props.remixes}
|
||||
report={this.state.report}
|
||||
addToStudioOpen={this.state.addToStudioOpen}
|
||||
projectStudios={this.props.projectStudios}
|
||||
curatedStudios={this.props.curatedStudios}
|
||||
onToggleStudio={this.handleToggleStudio}
|
||||
studios={this.props.studios}
|
||||
user={this.props.user}
|
||||
userOwnsProject={this.userOwnsProject()}
|
||||
onFavoriteClicked={this.handleFavoriteToggle}
|
||||
|
@ -319,6 +333,9 @@ class Preview extends React.Component {
|
|||
onReportClicked={this.handleReportClick}
|
||||
onReportClose={this.handleReportClose}
|
||||
onReportSubmit={this.handleReportSubmit}
|
||||
onAddToStudioClicked={this.handleAddToStudioClick}
|
||||
onAddToStudioClosed={this.handleAddToStudioClose}
|
||||
onToggleStudio={this.handleToggleStudio}
|
||||
onSeeInside={this.handleSeeInside}
|
||||
onUpdate={this.handleUpdate}
|
||||
/>
|
||||
|
@ -358,7 +375,7 @@ Preview.propTypes = {
|
|||
setLovedStatus: PropTypes.func.isRequired,
|
||||
setPlayer: PropTypes.func.isRequired,
|
||||
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
curatedStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
studios: PropTypes.arrayOf(PropTypes.object),
|
||||
updateProject: PropTypes.func.isRequired,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
|
@ -381,36 +398,40 @@ Preview.defaultProps = {
|
|||
// 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 consolidatedStudios = [];
|
||||
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) => {
|
||||
curatedStudios.forEach(curatedStudio => {
|
||||
let studioCopy = Object.assign({}, curatedStudio, {includesProject: false});
|
||||
projectStudios.some(projectStudio => {
|
||||
if (curatedStudio.id === projectStudio.id) {
|
||||
studioCopy.includesProject = true;
|
||||
studioCopy.includesProject = projectStudio.includesProject;
|
||||
projectStudiosFoundInCurated[projectStudio.id] = true;
|
||||
return true; // break out of the Array.some loop
|
||||
}
|
||||
});
|
||||
// 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);
|
||||
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 public studios, add to front of list
|
||||
projectStudios.forEach((projectStudio) => {
|
||||
// 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 => {
|
||||
if (!(projectStudio.id in projectStudiosFoundInCurated)) {
|
||||
studios.unshift(Object.assign({}, projectStudio,
|
||||
{includesProject: true, hasRequestOutstanding: false}
|
||||
));
|
||||
// no need to specify includesProject = true or false, because
|
||||
// that state is managed by redux actions.
|
||||
consolidatedStudios.unshift(Object.assign({}, projectStudio));
|
||||
}
|
||||
});
|
||||
return studios;
|
||||
// set studio state to hasRequestOutstanding==true if it's being fetched,
|
||||
// false if it's not
|
||||
consolidatedStudios.forEach(consolidatedStudio => {
|
||||
const id = consolidatedStudio.id;
|
||||
consolidatedStudio.hasRequestOutstanding =
|
||||
((id in studioRequests) &&
|
||||
(studioRequests[id] === previewActions.Status.FETCHING));
|
||||
});
|
||||
return consolidatedStudios;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -423,7 +444,7 @@ const mapStateToProps = state => ({
|
|||
remixes: state.preview.remixes,
|
||||
sessionStatus: state.session.status,
|
||||
projectStudios: state.preview.projectStudios,
|
||||
curatedStudios: consolidateStudiosInfo(state.preview.curatedStudios,
|
||||
studios: consolidateStudiosInfo(state.preview.curatedStudios,
|
||||
state.preview.projectStudios, state.preview.status.studioRequests),
|
||||
user: state.session.session.user,
|
||||
playerMode: state.scratchGui.mode.isPlayerOnly,
|
||||
|
|
Loading…
Reference in a new issue