Show details about why a project is not public

Follow up the project info request with a request to the visibility endpoint to find out if the project is trashed or censored. The project just not being published is handled by the existing code.

This PR generalizes the ShareBanner to a more generic "Banner" that is then filled with the relevant content.
This commit is contained in:
Paul Kaplan 2018-11-20 11:59:50 -05:00
parent af301ba450
commit 122160726d
9 changed files with 167 additions and 78 deletions

View file

@ -8,6 +8,9 @@ $ui-orange: hsla(38, 100, 55, 1); // #FFAB19 Control Primary
$ui-orange-10percent: hsla(35, 90, 55, .1);
$ui-orange-25percent: hsla(35, 90, 55, .25);
$ui-red: hsla(20, 100%, 55%, 1); /* #FF661A */
$ui-red-25percent: hsla(20, 100%, 55%, .25);
$ui-light-gray: hsla(0, 0, 98, 1); //#FAFAFA
$ui-gray: hsla(0, 0, 95, 1); //#F2F2F2
$ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3

View file

@ -25,6 +25,7 @@ module.exports.getInitialState = () => ({
report: module.exports.Status.NOT_FETCHED,
projectStudios: module.exports.Status.NOT_FETCHED,
curatedStudios: module.exports.Status.NOT_FETCHED,
visibility: module.exports.Status.NOT_FETCHED,
studioRequests: {}
},
projectInfo: {},
@ -39,7 +40,8 @@ module.exports.getInitialState = () => ({
curatedStudios: [],
currentStudioIds: [],
moreCommentsToLoad: false,
projectNotAvailable: false
projectNotAvailable: false,
visibilityInfo: {}
});
module.exports.previewReducer = (state, action) => {
@ -169,6 +171,10 @@ module.exports.previewReducer = (state, action) => {
return Object.assign({}, state, {
moreCommentsToLoad: action.moreCommentsToLoad
});
case 'SET_VISIBILITY_INFO':
return Object.assign({}, state, {
visibilityInfo: action.visibilityInfo
});
case 'ERROR':
log.error(action.error);
return state;
@ -321,7 +327,12 @@ module.exports.resetComments = () => ({
type: 'RESET_COMMENTS'
});
module.exports.getProjectInfo = (id, token) => (dispatch => {
module.exports.setVisibilityInfo = visibilityInfo => ({
type: 'SET_VISIBILITY_INFO',
visibilityInfo: visibilityInfo
});
module.exports.getProjectInfo = (id, username, token) => (dispatch => {
const opts = {
uri: `/projects/${id}`
};
@ -343,6 +354,27 @@ module.exports.getProjectInfo = (id, token) => (dispatch => {
}
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHED));
dispatch(module.exports.setProjectInfo(body));
// If the project is not public, make a follow-up request for why
if (username && !body.public) {
dispatch(module.exports.getVisibilityInfo(id, username, token));
}
});
});
module.exports.getVisibilityInfo = (id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('visibility', module.exports.Status.FETCHING));
api({
uri: `/users/${username}/projects/${id}/visibility`,
authentication: token
}, (err, body, response) => {
if (err || !body || response.statusCode !== 200) {
dispatch(module.exports.setFetchStatus('visibility', module.exports.Status.ERROR));
dispatch(module.exports.setError('No visibility info available'));
return;
}
dispatch(module.exports.setFetchStatus('visibility', module.exports.Status.FETCHED));
dispatch(module.exports.setVisibilityInfo(body));
});
});

View file

@ -0,0 +1,34 @@
const PropTypes = require('prop-types');
const React = require('react');
const classNames = require('classnames');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Button = require('../../components/forms/button.jsx');
require('./banner.scss');
const Banner = ({className, message, actionMessage, onAction}) => (
<div className={classNames('banner-outer', className)}>
<FlexRow className="inner banner-inner">
<span className="banner-text">
{message}
</span>
{actionMessage && onAction && (
<Button
className="button banner-button"
onClick={onAction}
>
{actionMessage}
</Button>
)}
</FlexRow>
</div>
);
Banner.propTypes = {
actionMessage: PropTypes.node,
className: PropTypes.string,
message: PropTypes.node.isRequired,
onAction: PropTypes.func
};
module.exports = Banner;

View file

@ -0,0 +1,36 @@
@import "../../colors";
$navigation-height: 50px;
.banner-outer {
background-color: $ui-orange-25percent;
width: 100%;
overflow: hidden;
color: $ui-orange;
font-weight: bold;
}
.banner-outer.banner-danger {
background-color: $ui-red-25percent;
color: $ui-red;
}
.banner-inner {
min-height: 60px;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
}
.banner-text {
padding: 0.5rem 0;
}
.banner-button {
background-color: $ui-orange;
font-size: .875rem;
}
.banner-danger .banner-button {
background-color: $ui-red;
}

View file

@ -20,5 +20,6 @@
"project.inviteToRemix": "Invite user to remix",
"project.instructionsLabel": "Instructions",
"project.notesAndCreditsLabel": "Notes and Credits",
"project.credit": "Thanks to {userLink} for the original project {projectLink}."
"project.credit": "Thanks to {userLink} for the original project {projectLink}.",
"project.deletedBanner": "Note: This project is in the trash folder"
}

View file

@ -15,7 +15,7 @@ const decorateText = require('../../lib/decorate-text.jsx');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Button = require('../../components/forms/button.jsx');
const Avatar = require('../../components/avatar/avatar.jsx');
const ShareBanner = require('./share-banner.jsx');
const Banner = require('./banner.jsx');
const RemixCredit = require('./remix-credit.jsx');
const RemixList = require('./remix-list.jsx');
const Stats = require('./stats.jsx');
@ -96,17 +96,50 @@ const PreviewPresentation = ({
replies,
reportOpen,
singleCommentId,
userOwnsProject
userOwnsProject,
visibilityInfo
}) => {
const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : '';
// Allow embedding html in banner messages coming from the server
const embedCensorMessage = message => (
// eslint-disable-next-line react/no-danger
<span dangerouslySetInnerHTML={{__html: message}} />
);
let banner;
if (visibilityInfo.censored) {
if (visibilityInfo.reshareable) {
banner = (<Banner
actionMessage={<FormattedMessage id="project.share.shareButton" />}
className="banner-danger"
message={embedCensorMessage(visibilityInfo.censorMessage)}
onAction={onShare}
/>);
} else {
banner = (<Banner
className="banner-danger"
message={embedCensorMessage(visibilityInfo.censorMessage)}
/>);
}
} else if (visibilityInfo.deleted) {
banner = (<Banner
className="banner-danger"
message={<FormattedMessage id="project.deletedBanner" />}
/>);
} else if (canShare && !isShared) {
banner = (<Banner
actionMessage={<FormattedMessage id="project.share.shareButton" />}
message={<FormattedMessage id="project.share.notShared" />}
onAction={onShare}
/>);
}
return (
<div className="preview">
{canShare && !isShared && (
<ShareBanner onShare={onShare} />
)}
{ projectInfo && projectInfo.author && projectInfo.author.id && (
<React.Fragment>
{banner}
<div className="inner">
<FlexRow className="preview-row force-row">
<FlexRow className="project-header">
@ -507,7 +540,13 @@ PreviewPresentation.propTypes = {
replies: PropTypes.objectOf(PropTypes.array),
reportOpen: PropTypes.bool,
singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
userOwnsProject: PropTypes.bool
userOwnsProject: PropTypes.bool,
visibilityInfo: PropTypes.shape({
censored: PropTypes.bool,
censorMessage: PropTypes.string,
deleted: PropTypes.bool,
reshareable: PropTypes.bool
})
};
module.exports = injectIntl(PreviewPresentation);

View file

@ -140,7 +140,7 @@ class Preview extends React.Component {
this.props.getTopLevelComments(this.state.projectId, this.props.comments.length,
this.props.isAdmin, token);
}
this.props.getProjectInfo(this.state.projectId, token);
this.props.getProjectInfo(this.state.projectId, username, token);
this.props.getRemixes(this.state.projectId, token);
this.props.getProjectStudios(this.state.projectId, token);
this.props.getCuratedStudios(username);
@ -459,6 +459,7 @@ class Preview extends React.Component {
reportOpen={this.state.reportOpen}
singleCommentId={this.state.singleCommentId}
userOwnsProject={this.props.userOwnsProject}
visibilityInfo={this.props.visibilityInfo}
onAddComment={this.handleAddComment}
onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose}
@ -597,7 +598,13 @@ Preview.propTypes = {
classroomId: PropTypes.string
}),
userOwnsProject: PropTypes.bool,
userPresent: PropTypes.bool
userPresent: PropTypes.bool,
visibilityInfo: PropTypes.shape({
censored: PropTypes.bool,
censorMessage: PropTypes.string,
deleted: PropTypes.bool,
reshareable: PropTypes.bool
})
};
Preview.defaultProps = {
@ -665,7 +672,8 @@ const mapStateToProps = state => {
sessionStatus: state.session.status, // check if used
user: state.session.session.user,
userOwnsProject: userOwnsProject,
userPresent: userPresent
userPresent: userPresent,
visibilityInfo: state.preview.visibilityInfo
};
};
@ -707,8 +715,8 @@ const mapDispatchToProps = dispatch => ({
getParentInfo: id => {
dispatch(previewActions.getParentInfo(id));
},
getProjectInfo: (id, token) => {
dispatch(previewActions.getProjectInfo(id, token));
getProjectInfo: (id, username, token) => {
dispatch(previewActions.getProjectInfo(id, username, token));
},
getRemixes: id => {
dispatch(previewActions.getRemixes(id));

View file

@ -1,29 +0,0 @@
const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Button = require('../../components/forms/button.jsx');
require('./share-banner.scss');
const ShareBanner = ({onShare}) => (
<div className="share-banner-outer">
<FlexRow className="inner share-banner">
<span className="share-text">
<FormattedMessage id="project.share.notShared" />
</span>
<Button
className="button share-button"
onClick={onShare}
>
<FormattedMessage id="project.share.shareButton" />
</Button>
</FlexRow>
</div>
);
ShareBanner.propTypes = {
onShare: PropTypes.func
};
module.exports = ShareBanner;

View file

@ -1,35 +0,0 @@
@import "../../colors";
$navigation-height: 50px;
.share-banner-outer {
background-color: $ui-orange-25percent;
width: 100%;
overflow: hidden;
color: $ui-orange;
}
.share-banner {
align-items: center;
justify-content: space-between;
}
.share-button {
background-color: $ui-orange;
font-size: .875rem;
font-weight: normal;
// don't show an image in share button, for now.
// &:before {
// display: inline-block;
// margin-right: .5rem;
// background-image: url("/svgs/project/share-white.svg");
// background-repeat: no-repeat;
// background-position: center center;
// background-size: contain;
// width: 1.25rem;
// height: 1.25rem;
// vertical-align: middle;
// content: "";
// }
}