Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move styles inline for React progress components #8756

Merged
merged 14 commits into from Jun 7, 2016
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion code-studio/src/js/components/progress/course_progress.jsx
Expand Up @@ -10,13 +10,14 @@ import StageDetails from './stage_details.jsx';
*/
var CourseProgress = React.createClass({
propTypes: {
currentLevelId: React.PropTypes.string,
display: React.PropTypes.oneOf(['dots', 'list']).isRequired,
stages: React.PropTypes.arrayOf(STAGE_TYPE)
},

getRow(stage) {
if (this.props.display === 'dots') {
return <CourseProgressRow stage={stage} key={stage.name} />;
return <CourseProgressRow stage={stage} key={stage.name} currentLevelId={this.props.currentLevelId} />;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, dumb question.. is it normal to use these kinds of custom tags in react?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@breville This is how you pass props to child components (i.e. The CourseProgressRow component will now have a prop named currentLevelId set to the same value as this component's currentLevelId

} else {
return <StageDetails stage={stage} key={stage.name} />;
}
Expand All @@ -43,6 +44,7 @@ var CourseProgress = React.createClass({
});

export default connect(state => ({
currentLevelId: state.currentLevelId,
display: state.display,
stages: state.stages
}))(CourseProgress);
24 changes: 21 additions & 3 deletions code-studio/src/js/components/progress/course_progress_row.jsx
Expand Up @@ -2,29 +2,47 @@
import React from 'react';
import { STAGE_TYPE } from './types';
import StageProgress from './stage_progress.jsx';
import color from '../../color';

var style = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could be const. also our convention has been to call this variable styles rather than style.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c149942cf4e47f8b6c9508cc71a2d89bb899558a.

row: {
borderBottom: `2px solid ${color.lighter_gray}`,
display: 'table',
tableLayout: 'fixed',
padding: '10px',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could be 10 instead of 10px

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c149942cf4e47f8b6c9508cc71a2d89bb899558a.

width: '100%'
},
stageName: {
display: 'table-cell',
width: '200px',
verticalAlign: 'middle',
paddingRight: '10px'
}
};

/**
* Stage progress component used in level header and course overview.
*/
var CourseProgressRow = React.createClass({
propTypes: {
currentLevelId: React.PropTypes.string,
stage: STAGE_TYPE
},

render() {
var stage = this.props.stage;

return (
<div className='game-group'>
<div className='stage'>
<div style={style.row}>
<div style={style.stageName}>
{stage.title}
<div className='stage-lesson-plan-link' style={{display: 'none'}}>
<a target='_blank' href={stage.lesson_plan_html_url}>
{dashboard.i18n.t('view_lesson_plan')}
</a>
</div>
</div>
<StageProgress levels={stage.levels} largeDots={true} saveAnswersFirst={false} />
<StageProgress levels={stage.levels} currentLevelId={this.props.currentLevelId} largeDots={true} saveAnswersFirst={false} />
</div>
);
}
Expand Down
140 changes: 115 additions & 25 deletions code-studio/src/js/components/progress/stage_progress.jsx
@@ -1,14 +1,103 @@
import Radium from 'radium';
import React from 'react';
import { STAGE_PROGRESS_TYPE } from './types';
import { saveAnswersAndNavigate } from '../../levels/saveAnswers';
import color from '../../color';

var style = {
overviewContainer: {
display: 'table-cell',
verticalAlign: 'middle',
paddingRight: '10px'
},
headerContainer: {
padding: '5px 8px',
backgroundColor: color.lightest_gray,
border: `1px solid ${color.lighter_gray}`,
borderRadius: '5px'
},
dot: {
puzzle: {
display: 'inline-block',
width: '24px',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to use variables for some of these values that are repeated a few times?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also many of them can be numbers instead of strings with px

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit-less numbers will always have 'px' appended, even things like fontSize? https://facebook.github.io/react/tips/style-props-value-px.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a shorthand for '5px 8px'?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything not in the list from your link will automatically get px. The one that's gotten me a few times is lineHeight which does not auto-get px.

idk about shorthand. Could try 5 8 but I bet it doesnt work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on lineHeight, I originally made this mistake before I saw your comment:

lineheight_bug

It makes me a little nervous that this mistake is so easy to make. I almost prefer always including units to avoid ambiguity, but I see the benefits of being able to do math with the shorthand.

height: '24px',
fontSize: '14px',
textAlign: 'center',
lineHeight: '24px',
borderRadius: '24px',
borderWidth: '2px',
borderStyle: 'solid',
borderColor: color.lighter_gray,
margin: '0 -1px',
transition: 'background-color .2s ease-out, border-color .2s ease-out, color .2s ease-out',
':hover': {
textDecoration: 'none',
color: color.white,
backgroundColor: color.level_current
}
},
unplugged: {
width: 'auto',
fontSize: '13px',
padding: '0 10px'
},
assessment: {
borderColor: color.assessment
},
small: {
width: '7px',
height: '7px',
borderRadius: '7px',
lineHeight: 'inherit',
fontSize: 0
},
overview: {
height: '30px',
width: '30px',
margin: '2px',
fontSize: '16px',
lineHeight: '32px'
}
},
status: {
submitted: {
color: color.white,
backgroundColor: color.level_submitted
},
perfect: {
color: color.white,
backgroundColor: color.level_perfect
},
passed: {
color: color.white,
backgroundColor: color.level_passed
},
attempted: {
color: color.charcoal,
backgroundColor: color.level_attempted
},
not_tried: {
color: color.charcoal,
backgroundColor: color.level_not_tried
},
review_rejected: {
color: color.white,
backgroundColor: color.level_review_rejected
},
review_accepted: {
color: color.white,
backgroundColor: color.level_review_accepted
}
}
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the neat thing about having these styles in .js is that we can hoist them up to a more common component if we end up using them elsewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though it is amusing to see the end of .css/.scss and the return of putting styles near the rendering itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also wonder if dot has enough complexity that it deserves its own component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted a ProgressDot React component in 97aad5987dfd56164e3d90c1ab01b55d6e427435.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: CSS, I miss the "Cascading" part of Cascading Style Sheets. It used to be that we could set one class on a root element and easily apply a totally different set of styles for many elements (e.g. #teacher-course for PLC script overview).

I like that components own all their styles and can be included with no extra dependencies, but it seems like we've lost some of the power of CSS. Outside CSS can still have unintended effects on components. Maybe Shadow DOM will make this better in the future?


/**
* Stage progress component used in level header and course overview.
*/
var StageProgress = React.createClass({
propTypes: {
levels: STAGE_PROGRESS_TYPE,
currentLevelIndex: React.PropTypes.number,
currentLevelId: React.PropTypes.string,
largeDots: React.PropTypes.bool,
saveAnswersFirst: React.PropTypes.bool.isRequired
},
Expand All @@ -20,24 +109,26 @@ var StageProgress = React.createClass({
},

render() {
var progressDots = this.props.levels.map((level, index) => {
var progressDots = this.props.levels.map(level => {
var uid = level.uid || level.id.toString();

var innerClass = 'level_link ' + (level.status || 'not_tried');
if (level.kind === 'unplugged') {
innerClass += ' unplugged_level';
var dotStyle = Object.assign({}, style.dot.puzzle);
if (level.kind === 'assessment') {
Object.assign(dotStyle, style.dot.assessment);
}

var outerClass = (level.kind === 'assessment') ? 'puzzle_outer_assessment' : 'puzzle_outer_level';
outerClass = (index === this.props.currentLevelIndex) ? 'puzzle_outer_current' : outerClass;

var isUnplugged = isNaN(level.title);
var dotStyle = {};
if (this.props.largeDots) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a comment somewhere explaining what large dots vs. small dots are? must admit i'm not quite sure what the difference is.

if (isUnplugged) {
dotStyle = {fontSize: '13px', padding: '4px 10px', margin: '1px'};
} else {
dotStyle = {padding: '5px 4px 3px 4px', margin: '1px'};
Object.assign(dotStyle, style.dot.overview);
if (uid === this.props.currentLevelId) {
Object.assign(dotStyle, {borderColor: color.level_current});
}
} else if (uid !== this.props.currentLevelId) {
Object.assign(dotStyle, style.dot.small);
}

var isUnplugged = isNaN(level.title);
if (isUnplugged) {
Object.assign(dotStyle, style.dot.unplugged);
}

var onClick = null;
Expand All @@ -47,24 +138,23 @@ var StageProgress = React.createClass({
}

return ([
<div className={outerClass}>
<a
href={level.url}
onClick={onClick}
className={innerClass + ' level-' + level.id}
style={dotStyle}>
{level.title}
</a>
</div>,
<a
key={uid}
className={`level-${level.id}`}
href={level.url}
onClick={onClick}
style={[dotStyle, style.status[level.status || 'not_tried']]}>
{level.title}
</a>,
' '
]);
});

return (
<div className={this.props.largeDots ? 'games' : 'progress_container'}>
<div className='react_stage' style={this.props.largeDots ? style.overviewContainer : style.headerContainer}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is overview vs. header the difference between drop-down view and in-header stage view, or something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! I'll rename these to something better.

{progressDots}
</div>
);
}
});
module.exports = StageProgress;
module.exports = Radium(StageProgress);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could use const/let vs. var throughout (tho not a requirement that you change it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c149942cf4e47f8b6c9508cc71a2d89bb899558a.

2 changes: 1 addition & 1 deletion code-studio/src/js/header.js
Expand Up @@ -135,7 +135,7 @@ header.build = function (stageData, progressData, currentLevelId, scriptName, pu
function lazyLoadPopup() {
if (!popupLoaded) {
popupLoaded = true;
$.getJSON(`/api/script_structure/${scriptName}`, progress.renderCourseProgress);
$.getJSON(`/api/script_structure/${scriptName}`, data => progress.renderCourseProgress(data, currentLevelId));
}
}
};
Expand Down
9 changes: 7 additions & 2 deletions code-studio/src/js/initApp/loadApp.js
Expand Up @@ -4,6 +4,7 @@ var renderAbusive = require('./renderAbusive');
var userAgentParser = require('./userAgentParser');
var progress = require('../progress');
var clientState = require('../clientState');
var color = require('../color');

// Max milliseconds to wait for last attempt data from the server
var LAST_ATTEMPT_TIMEOUT = 5000;
Expand Down Expand Up @@ -67,8 +68,12 @@ module.exports = function (callback) {
if (serverProgress[levelId] !== clientProgress[levelId]) {
var status = progress.mergedActivityCssClass(clientProgress[levelId], serverProgress[levelId]);

// Clear the existing class and replace
$('.level-' + levelId).attr('class', 'level_link ' + status);
// Set the progress color
var css = {backgroundColor: color[`level_${status}`] || color.level_not_tried};
if (status && status !== 'not_tried' && status !== 'attempted') {
Object.assign(css, {color: color.white});
}
$('.level-' + levelId).css(css);

// Write down new progress in sessionStorage
clientState.trackProgress(null, null, serverProgress[levelId], appOptions.scriptName, levelId);
Expand Down
24 changes: 8 additions & 16 deletions code-studio/src/js/progress.js
Expand Up @@ -60,17 +60,10 @@ progress.mergedActivityCssClass = function (a, b) {

progress.renderStageProgress = function (stageData, progressData, clientProgress, currentLevelId, puzzlePage) {
var serverProgress = progressData.levels || {};
var currentLevelIndex = null;
var lastLevelId = null;
var levelRepeat = 0;

var combinedProgress = stageData.levels.map(function (level, index) {
// Determine the current level index.
// However, because long assessments can have the same level appearing
// multiple times, just set this the first time it's determined.
if (level.id === currentLevelId && currentLevelIndex === null) {
currentLevelIndex = index;
}
var combinedProgress = stageData.levels.map(function (level) {

// If we have a multi-page level, then we will encounter the same level ID
// multiple times in a row. Keep track of how many times we've seen it
Expand Down Expand Up @@ -112,26 +105,23 @@ progress.renderStageProgress = function (stageData, progressData, clientProgress
status: status,
kind: level.kind,
url: href,
uid: level.uid,
id: level.id
};
});

if (currentLevelIndex !== null && puzzlePage !== progress.PUZZLE_PAGE_NONE) {
currentLevelIndex += puzzlePage - 1;
}

var mountPoint = document.createElement('div');
mountPoint.style.display = 'inline-block';
$('.progress_container').replaceWith(mountPoint);
ReactDOM.render(React.createElement(StageProgress, {
levels: combinedProgress,
currentLevelIndex: currentLevelIndex,
currentLevelId: currentLevelId,
saveAnswersFirst: puzzlePage !== progress.PUZZLE_PAGE_NONE
}), mountPoint);
};

progress.renderCourseProgress = function (scriptData) {
var store = loadProgress(scriptData);
progress.renderCourseProgress = function (scriptData, currentLevelId) {
var store = loadProgress(scriptData, currentLevelId);
var mountPoint = document.createElement('div');

$.ajax('/api/user_progress/' + scriptData.name).done(data => {
Expand Down Expand Up @@ -160,13 +150,14 @@ progress.renderCourseProgress = function (scriptData) {
);
};

function loadProgress(scriptData) {
function loadProgress(scriptData, currentLevelId) {
var teacherCourse = $('#landingpage').hasClass('teacher-course');

let store = createStore((state = [], action) => {
if (action.type === 'MERGE_PROGRESS') {
let newProgress = {};
return {
currentLevelId: state.currentLevelId,
display: state.display,
progress: newProgress,
stages: state.stages.map(stage => _.assign({}, stage, {levels: stage.levels.map(level => {
Expand All @@ -179,6 +170,7 @@ function loadProgress(scriptData) {
}
return state;
}, {
currentLevelId: currentLevelId,
display: teacherCourse ? 'list' : 'dots',
progress: {},
stages: scriptData.stages
Expand Down
3 changes: 2 additions & 1 deletion dashboard/app/views/layouts/_header.html.haml
Expand Up @@ -63,8 +63,9 @@
- user_progress = current_user == nil ? 'null' : summarize_user_progress(script_level.script, view_as, view_as == current_user && @public_caching).to_json

- puzzle_page = params[:puzzle_page] || ApplicationHelper::PUZZLE_PAGE_NONE
- uid = params[:puzzle_page] ? "#{script_level.level_id}_#{puzzle_page.to_i - 1}" : script_level.level_id.to_s

:javascript
//<![CDATA[
dashboard.header.build(#{script_level.stage.summarize.to_json}, #{user_progress}, #{script_level.level_id}, "#{script.name}", #{puzzle_page})
dashboard.header.build(#{script_level.stage.summarize.to_json}, #{user_progress}, "#{uid}", "#{script.name}", #{puzzle_page})
//]]>
5 changes: 2 additions & 3 deletions dashboard/test/ui/features/hourOfCode.feature
Expand Up @@ -50,9 +50,8 @@ Scenario: Failing at puzzle 1, refreshing puzzle 1, bubble should show up as att
Then I wait to see ".modal"
And I close the dialog
When element "#runButton" is visible
Then element ".header_middle a.level_link:first" has class "attempted"
And I am on "http://studio.code.org/s/hourofcode"
And element ".user-stats-block a.level_link:first" has class "attempted"
And I verify progress in the header of the current page is "attempted" for level 1
And I navigate to the course page and verify progress for course "hourofcode" stage 1 level 1 is "attempted"

@no_mobile
Scenario: Go to puzzle 10, see video, go somewhere else, return to puzzle 10, should not see video, comes back on link
Expand Down