Merge pull request #2340 from paulkaplan/visibility-info

Show details about why a project is not public
This commit is contained in:
Paul Kaplan 2018-11-26 09:18:38 -05:00 committed by GitHub
commit 3fb5dd769a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 163 additions and 74 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-10percent: hsla(35, 90, 55, .1);
$ui-orange-25percent: hsla(35, 90, 55, .25); $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-light-gray: hsla(0, 0, 98, 1); //#FAFAFA
$ui-gray: hsla(0, 0, 95, 1); //#F2F2F2 $ui-gray: hsla(0, 0, 95, 1); //#F2F2F2
$ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3 $ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3

View file

@ -25,6 +25,7 @@ module.exports.getInitialState = () => ({
report: module.exports.Status.NOT_FETCHED, report: module.exports.Status.NOT_FETCHED,
projectStudios: module.exports.Status.NOT_FETCHED, projectStudios: module.exports.Status.NOT_FETCHED,
curatedStudios: module.exports.Status.NOT_FETCHED, curatedStudios: module.exports.Status.NOT_FETCHED,
visibility: module.exports.Status.NOT_FETCHED,
studioRequests: {} studioRequests: {}
}, },
projectInfo: {}, projectInfo: {},
@ -39,7 +40,8 @@ module.exports.getInitialState = () => ({
curatedStudios: [], curatedStudios: [],
currentStudioIds: [], currentStudioIds: [],
moreCommentsToLoad: false, moreCommentsToLoad: false,
projectNotAvailable: false projectNotAvailable: false,
visibilityInfo: {}
}); });
module.exports.previewReducer = (state, action) => { module.exports.previewReducer = (state, action) => {
@ -169,6 +171,10 @@ module.exports.previewReducer = (state, action) => {
return Object.assign({}, state, { return Object.assign({}, state, {
moreCommentsToLoad: action.moreCommentsToLoad moreCommentsToLoad: action.moreCommentsToLoad
}); });
case 'SET_VISIBILITY_INFO':
return Object.assign({}, state, {
visibilityInfo: action.visibilityInfo
});
case 'ERROR': case 'ERROR':
log.error(action.error); log.error(action.error);
return state; return state;
@ -321,6 +327,11 @@ module.exports.resetComments = () => ({
type: 'RESET_COMMENTS' type: 'RESET_COMMENTS'
}); });
module.exports.setVisibilityInfo = visibilityInfo => ({
type: 'SET_VISIBILITY_INFO',
visibilityInfo: visibilityInfo
});
module.exports.getProjectInfo = (id, token) => (dispatch => { module.exports.getProjectInfo = (id, token) => (dispatch => {
const opts = { const opts = {
uri: `/projects/${id}` 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.setFetchStatus('project', module.exports.Status.FETCHED));
dispatch(module.exports.setProjectInfo(body)); dispatch(module.exports.setProjectInfo(body));
// If the project is not public, make a follow-up request for why
if (!body.public) {
dispatch(module.exports.getVisibilityInfo(id, body.author.username, token));
}
});
});
module.exports.getVisibilityInfo = (id, ownerUsername, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('visibility', module.exports.Status.FETCHING));
api({
uri: `/users/${ownerUsername}/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: .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.inviteToRemix": "Invite user to remix",
"project.instructionsLabel": "Instructions", "project.instructionsLabel": "Instructions",
"project.notesAndCreditsLabel": "Notes and Credits", "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 FlexRow = require('../../components/flex-row/flex-row.jsx');
const Button = require('../../components/forms/button.jsx'); const Button = require('../../components/forms/button.jsx');
const Avatar = require('../../components/avatar/avatar.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 RemixCredit = require('./remix-credit.jsx');
const RemixList = require('./remix-list.jsx'); const RemixList = require('./remix-list.jsx');
const Stats = require('./stats.jsx'); const Stats = require('./stats.jsx');
@ -96,17 +96,50 @@ const PreviewPresentation = ({
replies, replies,
reportOpen, reportOpen,
singleCommentId, singleCommentId,
userOwnsProject userOwnsProject,
visibilityInfo
}) => { }) => {
const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : ''; 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.deleted) { // If both censored and deleted, prioritize deleted banner
banner = (<Banner
className="banner-danger"
message={<FormattedMessage id="project.deletedBanner" />}
/>);
} else 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 (canShare && !isShared) {
banner = (<Banner
actionMessage={<FormattedMessage id="project.share.shareButton" />}
message={<FormattedMessage id="project.share.notShared" />}
onAction={onShare}
/>);
}
return ( return (
<div className="preview"> <div className="preview">
{canShare && !isShared && (
<ShareBanner onShare={onShare} />
)}
{ projectInfo && projectInfo.author && projectInfo.author.id && ( { projectInfo && projectInfo.author && projectInfo.author.id && (
<React.Fragment> <React.Fragment>
{banner}
<div className="inner"> <div className="inner">
<FlexRow className="preview-row force-row"> <FlexRow className="preview-row force-row">
<FlexRow className="project-header"> <FlexRow className="project-header">
@ -507,7 +540,13 @@ PreviewPresentation.propTypes = {
replies: PropTypes.objectOf(PropTypes.array), replies: PropTypes.objectOf(PropTypes.array),
reportOpen: PropTypes.bool, reportOpen: PropTypes.bool,
singleCommentId: PropTypes.oneOfType([PropTypes.number, 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); module.exports = injectIntl(PreviewPresentation);

View file

@ -459,6 +459,7 @@ class Preview extends React.Component {
reportOpen={this.state.reportOpen} reportOpen={this.state.reportOpen}
singleCommentId={this.state.singleCommentId} singleCommentId={this.state.singleCommentId}
userOwnsProject={this.props.userOwnsProject} userOwnsProject={this.props.userOwnsProject}
visibilityInfo={this.props.visibilityInfo}
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
onAddToStudioClicked={this.handleAddToStudioClick} onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose} onAddToStudioClosed={this.handleAddToStudioClose}
@ -597,7 +598,13 @@ Preview.propTypes = {
classroomId: PropTypes.string classroomId: PropTypes.string
}), }),
userOwnsProject: PropTypes.bool, 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 = { Preview.defaultProps = {
@ -665,7 +672,8 @@ const mapStateToProps = state => {
sessionStatus: state.session.status, // check if used sessionStatus: state.session.status, // check if used
user: state.session.session.user, user: state.session.session.user,
userOwnsProject: userOwnsProject, userOwnsProject: userOwnsProject,
userPresent: userPresent userPresent: userPresent,
visibilityInfo: state.preview.visibilityInfo
}; };
}; };

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: "";
// }
}