mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-12-04 12:51:28 -05:00
adding distinct object to redux/preview.js state to track studio membership
This commit is contained in:
parent
52281502b2
commit
ffe5e8cb43
3 changed files with 138 additions and 174 deletions
|
@ -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 = {
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue