diff --git a/apps/Gruntfile.js b/apps/Gruntfile.js index 56270b6737980..0dcd3dcc3135f 100644 --- a/apps/Gruntfile.js +++ b/apps/Gruntfile.js @@ -331,7 +331,7 @@ describe('entry tests', () => { var OUTPUT_DIR = 'build/package/js/'; config.exec = { convertScssVars: './script/convert-scss-variables.js', - generateSharedConstants: './script/generateSharedConstants.rb' + generateSharedConstants: 'bundle exec ./script/generateSharedConstants.rb' }; var junitReporterBaseConfig = { @@ -441,19 +441,10 @@ describe('entry tests', () => { var appsEntries = _.fromPairs(appsToBuild.map(function (app) { return [app, './src/sites/studio/pages/levels-' + app + '-main.js']; })); + var codeStudioEntries = { 'blockly': './src/sites/studio/pages/blockly.js', 'code-studio': './src/sites/studio/pages/code-studio.js', - 'levelbuilder': './src/sites/studio/pages/levelbuilder.js', - 'levelbuilder_applab': './src/sites/studio/pages/levelbuilder_applab.js', - 'levelbuilder_craft': './src/sites/studio/pages/levelbuilder_craft.js', - 'levelbuilder_edit_script': './src/sites/studio/pages/levelbuilder_edit_script.js', - 'levelbuilder_gamelab': './src/sites/studio/pages/levelbuilder_gamelab.js', - 'levelbuilder_studio': './src/sites/studio/pages/levelbuilder_studio.js', - 'levelbuilder_pixelation': './src/sites/studio/pages/levelbuilder_pixelation.js', - 'blocks/edit': './src/sites/studio/pages/blocks/edit.js', - 'shared_blockly_functions/edit':'./src/sites/studio/pages/shared_blockly_functions/edit.js', - 'libraries/edit': './src/sites/studio/pages/libraries/edit.js', 'levels/contract_match': './src/sites/studio/pages/levels/contract_match.jsx', 'levels/_curriculum_reference': './src/sites/studio/pages/levels/_curriculum_reference.js', 'levels/_dialog': './src/sites/studio/pages/levels/_dialog.js', @@ -465,10 +456,6 @@ describe('entry tests', () => { 'levels/textMatch': './src/sites/studio/pages/levels/textMatch.js', 'levels/widget': './src/sites/studio/pages/levels/widget.js', 'levels/_external_link': './src/sites/studio/pages/levels/_external_link.js', - 'levels/editors/_blockly': './src/sites/studio/pages/levels/editors/_blockly.js', - 'levels/editors/_droplet': './src/sites/studio/pages/levels/editors/_droplet.js', - 'levels/editors/_all': './src/sites/studio/pages/levels/editors/_all.js', - 'levels/editors/_dsl': './src/sites/studio/pages/levels/editors/_dsl.js', 'projects/index': './src/sites/studio/pages/projects/index.js', 'projects/public': './src/sites/studio/pages/projects/public.js', 'projects/featured': './src/sites/studio/pages/projects/featured.js', @@ -486,10 +473,27 @@ describe('entry tests', () => { 'congrats/index': './src/sites/studio/pages/congrats/index.js', 'courses/index': './src/sites/studio/pages/courses/index.js', 'courses/show': './src/sites/studio/pages/courses/show.js', - 'courses/edit': './src/sites/studio/pages/courses/edit.js', 'devise/registrations/edit': './src/sites/studio/pages/devise/registrations/edit.js', }; + var internalEntries = { + 'blocks/edit': './src/sites/studio/pages/blocks/edit.js', + 'courses/edit': './src/sites/studio/pages/courses/edit.js', + 'levelbuilder': './src/sites/studio/pages/levelbuilder.js', + 'levelbuilder_applab': './src/sites/studio/pages/levelbuilder_applab.js', + 'levelbuilder_craft': './src/sites/studio/pages/levelbuilder_craft.js', + 'levelbuilder_edit_script': './src/sites/studio/pages/levelbuilder_edit_script.js', + 'levelbuilder_gamelab': './src/sites/studio/pages/levelbuilder_gamelab.js', + 'levelbuilder_pixelation': './src/sites/studio/pages/levelbuilder_pixelation.js', + 'levelbuilder_studio': './src/sites/studio/pages/levelbuilder_studio.js', + 'levels/editors/_all': './src/sites/studio/pages/levels/editors/_all.js', + 'levels/editors/_blockly': './src/sites/studio/pages/levels/editors/_blockly.js', + 'levels/editors/_droplet': './src/sites/studio/pages/levels/editors/_droplet.js', + 'levels/editors/_dsl': './src/sites/studio/pages/levels/editors/_dsl.js', + 'libraries/edit': './src/sites/studio/pages/libraries/edit.js', + 'shared_blockly_functions/edit':'./src/sites/studio/pages/shared_blockly_functions/edit.js', + }; + var otherEntries = { essential: './src/sites/studio/pages/essential.js', plc: './src/sites/studio/pages/plc.js', @@ -527,6 +531,7 @@ describe('entry tests', () => { 'pd/application/principal_approval_application/new': './src/sites/studio/pages/pd/application/principal_approval_application/new.js', 'pd/teachercon1819_registration/new': './src/sites/studio/pages/pd/teachercon1819_registration/new.js', 'pd/fit_weekend1819_registration/new': './src/sites/studio/pages/pd/fit_weekend1819_registration/new.js', + 'pd/workshop_enrollment/new': './src/sites/studio/pages/pd/workshop_enrollment/new.js', 'pd/workshop_enrollment/cancel': './src/sites/studio/pages/pd/workshop_enrollment/cancel.js', 'pd/professional_learning_landing/index': './src/sites/studio/pages/pd/professional_learning_landing/index.js', @@ -571,6 +576,7 @@ describe('entry tests', () => { {}, appsEntries, codeStudioEntries, + internalEntries, otherEntries ), function (val) { @@ -617,6 +623,9 @@ describe('entry tests', () => { ...(process.env.ANALYZE_BUNDLE ? [ new BundleAnalyzerPlugin({ analyzerMode: 'static', + excludeAssets: [ + ...Object.keys(internalEntries), + ], }), ] : []), new StatsWriterPlugin({ diff --git a/apps/package.json b/apps/package.json index 6923b1c2478ba..d238a963f166d 100644 --- a/apps/package.json +++ b/apps/package.json @@ -43,9 +43,9 @@ "@cdo/apps": "file:src", "@cdo/interpreted": "link:../dashboard/config/libraries", "@code-dot-org/artist": "0.2.1", - "@code-dot-org/blockly": "3.0.2", + "@code-dot-org/blockly": "3.1.0", "@code-dot-org/bramble": "0.1.26", - "@code-dot-org/craft": "0.1.3", + "@code-dot-org/craft": "0.1.4", "@code-dot-org/johnny-five": "0.11.1-cdo.2", "@code-dot-org/js-interpreter-tyrant": "0.2.2", "@code-dot-org/js-numbers": "0.1.0-cdo.0", diff --git a/apps/script/generateSharedConstants.rb b/apps/script/generateSharedConstants.rb index 7657afda4aef5..e7ff8d7ef1744 100755 --- a/apps/script/generateSharedConstants.rb +++ b/apps/script/generateSharedConstants.rb @@ -66,6 +66,7 @@ def parse_raw(raw) def main shared_content = generate_multiple_constants %w( + ARTIST_AUTORUN_OPTIONS GAMELAB_AUTORUN_OPTIONS LEVEL_KIND LEVEL_STATUS diff --git a/apps/src/StudioApp.js b/apps/src/StudioApp.js index fd5baee112984..528ece7a27a34 100644 --- a/apps/src/StudioApp.js +++ b/apps/src/StudioApp.js @@ -837,6 +837,32 @@ StudioApp.prototype.reset = function (shouldPlayOpeningAnimation) { */ StudioApp.prototype.runButtonClick = function () {}; +StudioApp.prototype.addChangeHandler = function (newHandler) { + if (!this.changeHandlers) { + this.changeHandlers = []; + } + this.changeHandlers.push(newHandler); +}; + +StudioApp.prototype.runChangeHandlers = function () { + if (!this.changeHandlers) { + return; + } + this.changeHandlers.forEach(handler => handler()); +}; + +StudioApp.prototype.setupChangeHandlers = function () { + const runAllHandlers = this.runChangeHandlers.bind(this); + if (this.isUsingBlockly()) { + const blocklyCanvas = Blockly.mainBlockSpace.getCanvas(); + blocklyCanvas.addEventListener('blocklyBlockSpaceChange', runAllHandlers); + } else { + this.editor.on('change', runAllHandlers); + // Droplet doesn't automatically bubble up aceEditor changes + this.editor.aceEditor.on('change', runAllHandlers); + } +}; + /** * Toggle whether run button or reset button is shown * @param {string} button Button to show, either "run" or "reset" @@ -2062,6 +2088,7 @@ StudioApp.prototype.handleEditCode_ = function (config) { !!config.level.textModeAtStart ), }); + this.setupChangeHandlers(); if (config.level.paletteCategoryAtStart) { this.editor.changePaletteGroup(config.level.paletteCategoryAtStart); @@ -2449,6 +2476,7 @@ StudioApp.prototype.handleUsingBlockly_ = function (config) { }); this.inject(div, options); this.onResize(); + this.setupChangeHandlers(); if (config.afterInject) { config.afterInject(); diff --git a/apps/src/applab/applab.js b/apps/src/applab/applab.js index ed7fdb24db667..586f2546c6ce8 100644 --- a/apps/src/applab/applab.js +++ b/apps/src/applab/applab.js @@ -60,6 +60,7 @@ import { } from '../lib/tools/jsdebugger/redux'; import JavaScriptModeErrorHandler from '../JavaScriptModeErrorHandler'; import * as makerToolkit from '../lib/kits/maker/toolkit'; +import * as makerToolkitRedux from '../lib/kits/maker/redux'; import project from '../code-studio/initApp/project'; import * as thumbnailUtils from '../util/thumbnail'; import Sounds from '../Sounds'; @@ -1009,7 +1010,7 @@ Applab.execute = function () { } } - if (makerToolkit.isEnabled()) { + if (makerToolkitRedux.isEnabled(getStore().getState())) { makerToolkit.connect({ interpreter: Applab.JSInterpreter, onDisconnect: () => studioApp().resetButtonClick(), diff --git a/apps/src/code-studio/components/AudioRecorder.jsx b/apps/src/code-studio/components/AudioRecorder.jsx index 74ae0a7344f3b..887ac550f8eca 100644 --- a/apps/src/code-studio/components/AudioRecorder.jsx +++ b/apps/src/code-studio/components/AudioRecorder.jsx @@ -33,7 +33,8 @@ export default class AudioRecorder extends React.Component { this.slices = []; this.state = { audioName: "", - recording: false + recording: false, + cancelling: false }; } @@ -68,21 +69,34 @@ export default class AudioRecorder extends React.Component { }; saveAudio = (blob) => { - assetsApi.putAsset(this.state.audioName + ".mp3", blob, - (xhr) => { - this.setState({audioName: ""}); - this.props.onUploadDone(JSON.parse(xhr.response)); - this.props.afterAudioSaved(AudioErrorType.NONE); - }, error => { - console.error(`Audio Failed to Save: ${error}`); - this.props.afterAudioSaved(AudioErrorType.SAVE); - }); + if (!this.state.cancelling) { + assetsApi.putAsset(this.state.audioName + ".mp3", blob, + (xhr) => { + this.setState({audioName: ""}); + this.props.onUploadDone(JSON.parse(xhr.response)); + this.props.afterAudioSaved(AudioErrorType.NONE); + }, error => { + console.error(`Audio Failed to Save: ${error}`); + this.props.afterAudioSaved(AudioErrorType.SAVE); + }); + } }; onNameChange = (event) => { this.setState({audioName: event.target.value}); }; + onCancel = () => { + this.setState({audioName: "", recording: false, cancelling: true}, () => { + this.props.afterAudioSaved(AudioErrorType.NONE); + // Only stop recording if it's been started + if (this.recorder.state !== "inactive") { + this.recorder.stop(); + } + this.setState({cancelling: false}); + }); + }; + toggleRecord = () => { if (this.state.recording) { this.stopRecording(); @@ -129,7 +143,7 @@ export default class AudioRecorder extends React.Component { disabled={this.state.audioName.length === 0} /> +
+
+
+
+ + ); + } + + getMissingRequiredFields() { + const requiredFields = ['first_name', 'last_name', 'email']; + + if (!this.props.email) { + requiredFields.push('confirm_email'); + } + + if (this.props.workshop_course === CSF) { + requiredFields.push('role'); + } + + if (TEACHING_ROLES.includes(this.state.role)) { + requiredFields.push('grades_teaching'); + } + + const missingRequiredFields = requiredFields.filter(f => { + return !this.state[f]; + }); + + return missingRequiredFields; + } + + getErrors() { + const errors = {}; + + if (this.state.email) { + if (!isEmail(this.state.email)) { + errors.email = "Must be a valid email address"; + } + if (!this.props.email && this.state.email !== this.state.confirm_email) { + errors.confirm_email = "Email addresses do not match"; + } + } + + return errors; + } +} diff --git a/apps/src/code-studio/pd/workshop_enrollment/enrollmentConstants.jsx b/apps/src/code-studio/pd/workshop_enrollment/enrollmentConstants.jsx new file mode 100644 index 0000000000000..d55488ddfd484 --- /dev/null +++ b/apps/src/code-studio/pd/workshop_enrollment/enrollmentConstants.jsx @@ -0,0 +1,29 @@ +import {PropTypes} from 'react'; + +const WorkshopPropType = PropTypes.shape({ + id: PropTypes.number, + course: PropTypes.string, + course_url: PropTypes.string, + location_name: PropTypes.string, + location_address: PropTypes.string, + subject: PropTypes.string, + notes: PropTypes.string, + regional_partner: PropTypes.shape({ + name: PropTypes.string + }), + organizer: PropTypes.shape({ + name: PropTypes.string, + email: PropTypes.string + }), +}); + +const FacilitatorPropType = PropTypes.shape({ + email: PropTypes.string, + image_path: PropTypes.string, + bio: PropTypes.string +}); + +export { + WorkshopPropType, + FacilitatorPropType +}; diff --git a/apps/src/code-studio/pd/workshop_enrollment/facilitator_bio.jsx b/apps/src/code-studio/pd/workshop_enrollment/facilitator_bio.jsx new file mode 100644 index 0000000000000..23408dc188835 --- /dev/null +++ b/apps/src/code-studio/pd/workshop_enrollment/facilitator_bio.jsx @@ -0,0 +1,49 @@ +/** + * Facilitator bio as used on the workshop enrollment form + */ +import React from 'react'; +import marked from 'marked'; +import {FacilitatorPropType} from './enrollmentConstants'; + + +export default class FacilitatorBio extends React.Component { + static propTypes = { + facilitator: FacilitatorPropType + }; + + image = () => { + if (this.props.facilitator.image_path) { + return ; + } + }; + + bio = () => { + if (this.props.facilitator.bio) { + return ( +
+
+ ); + } else { + return ( +
+

{this.props.facilitator.name}

+

+ {this.props.facilitator.email} +

+
+ ); + } + }; + + render() { + return ( +
+ {this.image()} + {this.bio()} +
+
+ ); + } +} diff --git a/apps/src/code-studio/pd/workshop_enrollment/sign_in_prompt.jsx b/apps/src/code-studio/pd/workshop_enrollment/sign_in_prompt.jsx new file mode 100644 index 0000000000000..972be097ef0d1 --- /dev/null +++ b/apps/src/code-studio/pd/workshop_enrollment/sign_in_prompt.jsx @@ -0,0 +1,31 @@ +/* + * Info box prompting user to sign in on workshop enrollment page + */ +import React, {PropTypes} from 'react'; + +export default class SignInPrompt extends React.Component { + static propTypes = { + info_icon: PropTypes.string, + sign_in_url: PropTypes.string + }; + + render() { + return ( +
+
+ +
+
+

+ + Already have a Code.org account? + +

+

+ Sign in first to pre-fill some information. +

+
+
+ ); + } +} diff --git a/apps/src/code-studio/pd/workshop_enrollment/workshop_details.jsx b/apps/src/code-studio/pd/workshop_enrollment/workshop_details.jsx new file mode 100644 index 0000000000000..310ded3843d35 --- /dev/null +++ b/apps/src/code-studio/pd/workshop_enrollment/workshop_details.jsx @@ -0,0 +1,161 @@ +/** + * Workshop Details section of the workshop enrollment form + */ +import React, {PropTypes} from 'react'; +import {WorkshopPropType} from './enrollmentConstants'; + +const styles = { + label: { + textAlign: 'right' + }, + notes: { + whiteSpace: 'pre-wrap' + } +}; + +export default class WorkshopDetails extends React.Component { + static propTypes = { + workshop: WorkshopPropType, + session_dates: PropTypes.arrayOf(PropTypes.string) + }; + + workshopCourse() { + if (this.props.workshop.course_url) { + return ( + + {this.props.workshop.course} + + ); + } else { + return this.props.workshop.course; + } + } + + sessionDates() { + return ( +
+
+ + {this.props.session_dates.length === 1 ? 'Date:' : 'Dates:'} + +
+
+ {this.props.session_dates.map(date => ( +
+ {date} +
+
+ ))} +
+
+ ); + } + + location() { + return ( +
+
+ Location: +
+
+ {this.props.workshop.location_name} +
+ {this.props.workshop.location_address} +
+
+ ); + } + + courseAndSubject() { + return ( +
+
+ Course: +
+
+ {this.workshopCourse()} +
+ {this.props.workshop.subject} +
+
+ ); + } + + regionalPartner() { + if (this.props.workshop.regional_partner) { + return ( +
+
+ RegionalPartner: +
+
+ {this.props.workshop.regional_partner.name} +
+
+ ); + } + } + + organizerAndNotes() { + return ( +
+
+
+ Organizer Name: +
+
+ {this.props.workshop.organizer.name} +
+
+
+
+ Organizer Email: +
+
+ {this.props.workshop.organizer.email} +
+
+

+ {this.props.workshop.notes} +

+
+
+
+ ); + } + + render() { + return ( +
+
+
+

Workshop Details

+
+
+ {this.sessionDates()} + {this.location()} + {this.courseAndSubject()} + {this.regionalPartner()} + {this.organizerAndNotes()} +
+ ); + } +} diff --git a/apps/src/code-studio/pd/workshop_enrollment/workshop_enrollment.jsx b/apps/src/code-studio/pd/workshop_enrollment/workshop_enrollment.jsx new file mode 100644 index 0000000000000..35b4bf994c680 --- /dev/null +++ b/apps/src/code-studio/pd/workshop_enrollment/workshop_enrollment.jsx @@ -0,0 +1,245 @@ +/** + * New workshop enrollment page + */ +import React, {PropTypes} from 'react'; +import WorkshopDetails from './workshop_details'; +import FacilitatorBio from './facilitator_bio'; +import SignInPrompt from './sign_in_prompt'; +import EnrollForm from './enroll_form'; +import { + WorkshopPropType, + FacilitatorPropType +} from './enrollmentConstants'; + +const SUBMISSION_STATUSES = { + UNSUBMITTED: "unsubmitted", + DUPLICATE: "duplicate", + OWN: "own", + CLOSED: "closed", + FULL: "full", + NOT_FOUND: "not found", + SUCCESS: "success", + UNKNOWN_ERROR: "error" +}; + +export default class WorkshopEnrollment extends React.Component { + static propTypes = { + workshop: WorkshopPropType, + session_dates: PropTypes.arrayOf(PropTypes.string), + enrollment: PropTypes.shape({ + email: PropTypes.string, + first_name: PropTypes.string + }), + facilitators: PropTypes.arrayOf(FacilitatorPropType), + sign_in_prompt_data: PropTypes.shape({ + info_icon: PropTypes.string, + sign_in_url: PropTypes.string + }), + workshop_enrollment_status: PropTypes.string + }; + + constructor(props) { + super(props); + + this.state = { + workshopEnrollmentStatus: this.props.workshop_enrollment_status || SUBMISSION_STATUSES.UNSUBMITTED + }; + } + + onSubmissionComplete = (result) => { + if (result.responseJSON) { + this.setState({ + workshopEnrollmentStatus: result.responseJSON.workshop_enrollment_status, + cancelUrl: result.responseJSON.cancel_url, + accountExists: result.responseJSON.account_exists, + signUpUrl: result.responseJSON.sign_up_url, + workshopUrl: result.responseJSON.workshop_url + }); + } else { + this.setState({workshopEnrollmentStatus: SUBMISSION_STATUSES.UNKNOWN_ERROR}); + } + }; + + renderDuplicate() { + return ( +
+

+ Thank you for registering +

+

+ You are already registered, and should have received a confirmation email. +

+

+ If you need to cancel, click {this.state.cancelUrl} +

+
+ ); + } + + renderOwn() { + return ( +
+

+ You are attempting to join your own workshop. +

+
+ ); + } + + renderFull() { + return ( +
+

+ Sorry, this workshop is full. +

+

+ For more information, please contact the organizer: {this.props.workshop.organizer.email} +

+
+ ); + } + + renderClosed() { + return ( +
+

+ Sorry, this workshop is closed. +

+

+ For more information, please contact the organizer: {this.props.workshop.organizer.email} +

+
+ ); + } + + renderNotFound() { + return ( +
+

+ Sorry, this workshop could not be found. +

+
+ ); + } + + renderUnknownError() { + return ( +
+

+ Sorry, an error occurred and we were unable to enroll you in this workshop. + Please contact support@code.org. +

+
+ ); + } + + renderSuccess() { + return ( +
+

+ Thank you for registering +

+

+ You will receive a confirmation email. If you have any questions or need to + request special accommodations, please reach out directly to the workshop + organizer: {this.props.workshop.organizer.name} at {this.props.workshop.organizer.email}. +

+

+ If you need to cancel, click here. +

+
+ {!this.state.accountExists && +
+

+ Get a Head Start: Create Your Code.org Account +

+

+ If you don’t have a Code.org account yet, click below + to create one. You'll need a Code.org account on the day of the workshop. + You'll use this account to manage your students and view their progress + when you start teaching, so be sure to use the email you'll use when you + teach. +

+ + + + +
+ } +
+ ); + } + + render() { + switch (this.state.workshopEnrollmentStatus) { + case SUBMISSION_STATUSES.UNSUBMITTED: + return ( +
+

+ {`Register for a ${this.props.workshop.course} workshop`} +

+

+ Taught by Code.org facilitators who are experienced computer science educators, + our workshops will prepare you to teach the Code.org curriculum. +

+
+
+ {/* Left Column */} +
+ +

Facilitators

+ {this.props.facilitators.map(facilitator => ( + + ))} +
+ {/* Right Column */} +
+
+
+

Your Information

+ { + !this.props.enrollment.email && + + } + +
+
+
+
+
+
+ ); + case SUBMISSION_STATUSES.DUPLICATE: + return this.renderDuplicate(); + case SUBMISSION_STATUSES.OWN: + return this.renderOwn(); + case SUBMISSION_STATUSES.CLOSED: + return this.renderClosed(); + case SUBMISSION_STATUSES.FULL: + return this.renderFull(); + case SUBMISSION_STATUSES.NOT_FOUND: + return this.renderNotFound(); + case SUBMISSION_STATUSES.SUCCESS: + return this.renderSuccess(); + default: + return this.renderUnknownError(); + } + } +} diff --git a/apps/src/gamelab/GameLab.js b/apps/src/gamelab/GameLab.js index 485dd47ff0742..a8a80b95e7826 100644 --- a/apps/src/gamelab/GameLab.js +++ b/apps/src/gamelab/GameLab.js @@ -356,16 +356,7 @@ GameLab.prototype.init = function (config) { this.setCrosshairCursorForPlaySpace(); if (this.shouldAutoRunSetup) { - const changeHandler = this.rerunSetupCode.bind(this); - if (this.studioApp_.isUsingBlockly()) { - const blocklyCanvas = Blockly.mainBlockSpace.getCanvas(); - blocklyCanvas.addEventListener('blocklyBlockSpaceChange', - changeHandler); - } else { - this.studioApp_.editor.on('change', changeHandler); - // Droplet doesn't automatically bubble up aceEditor changes - this.studioApp_.editor.aceEditor.on('change', changeHandler); - } + this.studioApp_.addChangeHandler(this.rerunSetupCode.bind(this)); } }; diff --git a/apps/src/lib/kits/maker/redux.js b/apps/src/lib/kits/maker/redux.js index 5d09bd7dbc5b5..9ec8793e88312 100644 --- a/apps/src/lib/kits/maker/redux.js +++ b/apps/src/lib/kits/maker/redux.js @@ -26,6 +26,10 @@ export function isEnabled(state) { return getRoot(state).enabled; } +export function isAvailable(state) { + return !!(state && state.maker); +} + export function isConnecting(state) { return getRoot(state).connectionState === CONNECTING; } diff --git a/apps/src/lib/kits/maker/toolkit.js b/apps/src/lib/kits/maker/toolkit.js index dab263fb43ea7..a01a857f9cc7c 100644 --- a/apps/src/lib/kits/maker/toolkit.js +++ b/apps/src/lib/kits/maker/toolkit.js @@ -30,27 +30,12 @@ let currentBoard = null; * Enable Maker Toolkit for the current level. */ export function enable() { - if (!isAvailable()) { + if (!redux.isAvailable(getStore().getState())) { throw new MakerError('Maker cannot be enabled: Its reducer was not registered.'); } getStore().dispatch(redux.enable()); } -/** - * @returns {boolean} whether Maker Toolkit is enabled for the current level - */ -export function isEnabled() { - return redux.isEnabled(getStore().getState()); -} - -/** - * @returns {boolean} whether Maker Toolkit is usable with the current app at all - */ -export function isAvailable() { - const state = getStore().getState(); - return !!(state && state.maker); -} - /** * Called when starting execution of the student app. * Looks for a connected board, sets up an appropriate board controller, @@ -66,7 +51,7 @@ export function isAvailable() { * Rejects with another error type if something unexpected happens. */ export function connect({interpreter, onDisconnect}) { - if (!isEnabled()) { + if (!redux.isEnabled(getStore().getState())) { return Promise.reject(new Error('Attempted to connect to a maker board, ' + 'but Maker Toolkit is not enabled.')); } @@ -157,7 +142,7 @@ function shouldRunWithFakeBoard() { * and puts maker UI back in a default state. */ export function reset() { - if (!isEnabled()) { + if (!redux.isEnabled(getStore().getState())) { return; } diff --git a/apps/src/lib/kits/maker/ui/DiscountCodeSchoolChoice.jsx b/apps/src/lib/kits/maker/ui/DiscountCodeSchoolChoice.jsx index 20e24e57a4fe4..7c4f56cce8ce6 100644 --- a/apps/src/lib/kits/maker/ui/DiscountCodeSchoolChoice.jsx +++ b/apps/src/lib/kits/maker/ui/DiscountCodeSchoolChoice.jsx @@ -38,8 +38,8 @@ export default class DiscountCodeSchoolChoice extends Component { handleDropdownChange = (field, event) => { if (field === 'nces') { this.setState({ - schoolId: event.value, - schoolName: event.label, + schoolId: event ? event.value : '', + schoolName: event ? event.label : '', }); } }; diff --git a/apps/src/lib/ui/SettingsCog.jsx b/apps/src/lib/ui/SettingsCog.jsx index e23d216ecc14a..56ee59deb95ec 100644 --- a/apps/src/lib/ui/SettingsCog.jsx +++ b/apps/src/lib/ui/SettingsCog.jsx @@ -6,9 +6,10 @@ import FontAwesome from '../../templates/FontAwesome'; import color from '../../util/color'; import * as assets from '../../code-studio/assets'; import project from '../../code-studio/initApp/project'; -import * as maker from '../kits/maker/toolkit'; +import * as makerToolkitRedux from '../kits/maker/redux'; import PopUpMenu from './PopUpMenu'; import ConfirmEnableMakerDialog from "./ConfirmEnableMakerDialog"; +import {getStore} from '../../redux'; const style = { iconContainer: { @@ -69,7 +70,7 @@ class SettingsCog extends Component { toggleMakerToolkit = () => { this.close(); - if (!maker.isEnabled()) { + if (!makerToolkitRedux.isEnabled(getStore().getState())) { // Pop a confirmation dialog when trying to enable maker, // because we've had several users do this accidentally. this.showConfirmation(); @@ -158,12 +159,13 @@ ManageAssets.propTypes = { }; export function ToggleMaker(props) { - if (!maker.isAvailable()) { + const reduxState = getStore().getState(); + if (!makerToolkitRedux.isAvailable(reduxState)) { return null; } return ( - {maker.isEnabled() ? msg.disableMaker() : msg.enableMaker()} + {makerToolkitRedux.isEnabled(reduxState) ? msg.disableMaker() : msg.enableMaker()} ); } diff --git a/apps/src/lib/util/firehose.js b/apps/src/lib/util/firehose.js index 1f82612032e50..ce4ef7b321344 100644 --- a/apps/src/lib/util/firehose.js +++ b/apps/src/lib/util/firehose.js @@ -1,6 +1,8 @@ /** @file Provides clients to AWS Firehose, whose data is imported into AWS Redshift. */ -import AWS from 'aws-sdk'; +import AWS from 'aws-sdk/lib/core'; +import 'aws-sdk/lib/config'; +import Firehose from 'aws-sdk/clients/firehose'; import {createUuid, trySetLocalStorage, tryGetLocalStorage} from '@cdo/apps/utils'; import {getStore} from '@cdo/apps/redux'; @@ -255,6 +257,6 @@ class FirehoseClient { // eslint-disable-next-line const _0x12ed=['\x41\x4b\x49\x41\x4a\x41\x41\x4d\x42\x59\x4d\x36\x55\x53\x59\x54\x34\x35\x34\x51','\x78\x4e\x4e\x39\x4e\x79\x32\x61\x6d\x39\x78\x75\x4b\x79\x57\x39\x53\x2b\x4e\x76\x41\x77\x33\x67\x68\x68\x74\x68\x72\x6b\x37\x6b\x6e\x51\x59\x54\x77\x6d\x4d\x48','\x75\x73\x2d\x65\x61\x73\x74\x2d\x31','\x63\x6f\x6e\x66\x69\x67'];(function(_0xb54a92,_0x4e682a){var _0x44f3e8=function(_0x35c55a){while(--_0x35c55a){_0xb54a92['\x70\x75\x73\x68'](_0xb54a92['\x73\x68\x69\x66\x74']());}};_0x44f3e8(++_0x4e682a);}(_0x12ed,0x127));var _0xd12e=function(_0x2cedd5,_0x518781){_0x2cedd5=_0x2cedd5-0x0;var _0x4291ea=_0x12ed[_0x2cedd5];return _0x4291ea;};AWS[_0xd12e('0x0')]=new AWS['\x43\x6f\x6e\x66\x69\x67']({'\x61\x63\x63\x65\x73\x73\x4b\x65\x79\x49\x64':_0xd12e('0x1'),'\x73\x65\x63\x72\x65\x74\x41\x63\x63\x65\x73\x73\x4b\x65\x79':_0xd12e('0x2'),'\x72\x65\x67\x69\x6f\x6e':_0xd12e('0x3')}); -const FIREHOSE = new AWS.Firehose({apiVersion: '2015-08-04'}); +const FIREHOSE = new Firehose({apiVersion: '2015-08-04'}); const firehoseClient = new FirehoseClient(); export default firehoseClient; diff --git a/apps/src/sites/code.org/pages/public/teacher-dashboard/index.js b/apps/src/sites/code.org/pages/public/teacher-dashboard/index.js index 3f61e7784e4bf..eadd5eb2e3871 100644 --- a/apps/src/sites/code.org/pages/public/teacher-dashboard/index.js +++ b/apps/src/sites/code.org/pages/public/teacher-dashboard/index.js @@ -595,9 +595,11 @@ function main() { $scope.react_progress = true; $scope.$on('section-progress-rendered', () => { - $scope.section.$promise.then(script => - renderSectionProgress(script, $scope.script_list) - ); + $scope.section.$promise.then(script => { + $scope.script_list.$promise.then(validScripts => { + renderSectionProgress(script, validScripts); + }); + }); }); }]); @@ -631,7 +633,11 @@ function main() { $scope.react_text_responses = true; $scope.$on('text-responses-table-rendered', () => { - $scope.section.$promise.then(section => renderTextResponsesTable(section, $scope.script_list)); + $scope.section.$promise.then(section => { + $scope.script_list.$promise.then(validScripts => { + renderTextResponsesTable(section, validScripts); + }); + }); }); }]); diff --git a/apps/src/sites/studio/pages/pd/workshop_enrollment/new.js b/apps/src/sites/studio/pages/pd/workshop_enrollment/new.js new file mode 100644 index 0000000000000..1c279e8abeae0 --- /dev/null +++ b/apps/src/sites/studio/pages/pd/workshop_enrollment/new.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import WorkshopEnrollment from '@cdo/apps/code-studio/pd/workshop_enrollment/workshop_enrollment'; +import getScriptData from '@cdo/apps/util/getScriptData'; + +document.addEventListener('DOMContentLoaded', function () { + ReactDOM.render( + , + document.getElementById('enrollment-container'), + ); +}); diff --git a/apps/src/sites/studio/pages/projects/index.js b/apps/src/sites/studio/pages/projects/index.js index 0f235cb97d3bc..fe788fe9b1117 100644 --- a/apps/src/sites/studio/pages/projects/index.js +++ b/apps/src/sites/studio/pages/projects/index.js @@ -18,18 +18,26 @@ import { import projects, { selectGallery, setProjectLists, - prependProjects, setPersonalProjectsList, } from '@cdo/apps/templates/projects/projectsRedux'; -import publishDialogReducer, { - showPublishDialog, -} from '@cdo/apps/templates/projects/publishDialog/publishDialogRedux'; +import publishDialogReducer from '@cdo/apps/templates/projects/publishDialog/publishDialogRedux'; import deleteDialogReducer from '@cdo/apps/templates/projects/deleteDialog/deleteProjectDialogRedux'; -import { AlwaysPublishableProjectTypes, AllPublishableProjectTypes } from '@cdo/apps/util/sharedConstants'; +import firehoseClient from '@cdo/apps/lib/util/firehose'; + $(document).ready(() => { const script = document.querySelector('script[data-projects]'); const projectsData = JSON.parse(script.dataset.projects); + const studyGroup = experiments.isEnabled(experiments.CHEVRON_PUBLISH_EXPERIMENT) ? 'publish-chevron' : 'publish-button'; + + firehoseClient.putRecord( + { + study: 'project-publish', + study_group: studyGroup, + event: 'page-load', + user_id: projectsData.userId, + } + ); registerReducers({projects, publishDialog: publishDialogReducer, deleteDialog: deleteDialogReducer}); const store = getStore(); @@ -145,45 +153,17 @@ function showGallery(gallery) { $('#public-gallery-wrapper').toggle(gallery === Galleries.PUBLIC); } -// Make these available to angularProjects.js. These can go away -// once My Projects is moved to React. - -window.onShowConfirmPublishDialog = function (projectId, projectType) { - getStore().dispatch(showPublishDialog(projectId, projectType)); -}; - -window.AlwaysPublishableProjectTypes = AlwaysPublishableProjectTypes; - -window.AllPublishableProjectTypes = AllPublishableProjectTypes; - function setupReduxSubscribers(store) { let state = {}; store.subscribe(() => { let lastState = state; state = store.getState(); - // Update the project state and immediately add it to the public gallery - // when a PublishDialog state transition indicates that a project has just - // been published. - if ( - lastState.publishDialog && - lastState.publishDialog.lastPublishedAt !== - state.publishDialog.lastPublishedAt - ) { - window.setProjectPublishedAt( - state.publishDialog.projectId, - state.publishDialog.lastPublishedAt); - const projectData = state.publishDialog.lastPublishedProjectData; - const projectType = state.publishDialog.projectType; - store.dispatch(prependProjects([projectData], projectType)); - } - if ( (lastState.projects && lastState.projects.selectedGallery) !== (state.projects && state.projects.selectedGallery) ) { showGallery(state.projects.selectedGallery); } - }); } diff --git a/apps/src/templates/Notification.jsx b/apps/src/templates/Notification.jsx index 2036e07af134b..8af2457b025fd 100644 --- a/apps/src/templates/Notification.jsx +++ b/apps/src/templates/Notification.jsx @@ -132,6 +132,7 @@ class Notification extends Component { details: PropTypes.string.isRequired, detailsLinkText: PropTypes.string, detailsLink: PropTypes.string, + detailsLinkNewWindow: PropTypes.bool, buttonText: PropTypes.string, buttonLink: PropTypes.string, dismissible: PropTypes.bool.isRequired, @@ -192,6 +193,7 @@ class Notification extends Component { details, detailsLinkText, detailsLink, + detailsLinkNewWindow, type, buttonText, buttonLink, @@ -241,7 +243,11 @@ class Notification extends Component { {detailsLinkText && detailsLink && (   - + {detailsLinkText} diff --git a/apps/src/templates/SchoolAutocompleteDropdown.jsx b/apps/src/templates/SchoolAutocompleteDropdown.jsx index 02be97af186e4..eabf5f4c2d886 100644 --- a/apps/src/templates/SchoolAutocompleteDropdown.jsx +++ b/apps/src/templates/SchoolAutocompleteDropdown.jsx @@ -17,12 +17,16 @@ export default class SchoolAutocompleteDropdown extends Component { schoolFilter: PropTypes.func, }; + state = { + knownValue: null, + knownLabel: null + }; + static defaultProps = { fieldName: "nces_school_s", schoolFilter: () => true, }; - constructSchoolOption = school => ({ value: school.nces_id.toString(), label: `${school.name} - ${school.city}, ${school.state} ${school.zip}`, @@ -96,7 +100,31 @@ export default class SchoolAutocompleteDropdown extends Component { }); }; + onChange = (value) => { + if (value) { + // Cache the label for this value in case we need it for the next render. + this.setState({knownValue: value.value, knownLabel: value.label}); + } + this.props.onChange(value); + }; + render() { + // value will end up either an object or a string, depending whether we have + // a label or not. It appears to be the quirky behavior of react-select 1.x. + // See https://github.com/JedWatson/react-select/issues/865. + let value; + if (this.props.schoolDropdownOption) { + // Use the provided value & label object. + value = this.props.schoolDropdownOption; + } else if (this.props.value === this.state.knownValue) { + // Use the cached label for this value. + value = {value: this.props.value, label: this.state.knownLabel}; + } else { + // Use this value (typically an initial value). The label will be + // asychronously retrieved in this.getOptions(). + value = this.props.value; + } + return ( true} - value={this.props.schoolDropdownOption ? this.props.schoolDropdownOption : this.props.value} - onChange={this.props.onChange} + value={value} + onChange={this.onChange} placeholder={i18n.searchForSchool()} /> ); diff --git a/apps/src/templates/UnsafeRenderedMarkdown.jsx b/apps/src/templates/UnsafeRenderedMarkdown.jsx new file mode 100644 index 0000000000000..7e1bea736a159 --- /dev/null +++ b/apps/src/templates/UnsafeRenderedMarkdown.jsx @@ -0,0 +1,31 @@ +import React, { PropTypes } from 'react'; +import processMarkdown from 'marked'; +import renderer from "../util/StylelessRenderer"; + +/** + * Basic component for rendering a markdown string as HTML. + * + * Right now, it still uses marked; this will eventually be updated to use the + * new remark system, and possibly even support redaction. + * + * Note that this component will render anything contained in the markdown into + * the browser, including arbitrary html and script tags. It should be + * considered unsafe to use for user-generated content until the markdown + * renderer driving this component can be made safe. + */ +export default class UnsafeRenderedMarkdown extends React.Component { + static propTypes = { + markdown: PropTypes.string.isRequired + }; + + render() { + const processedMarkdown = processMarkdown(this.props.markdown, { renderer }); + /* eslint-disable react/no-danger */ + return ( +
+ ); + /* eslint-enable react/no-danger */ + } +} diff --git a/apps/src/templates/census2017/SchoolAutocompleteDropdownWithLabel.jsx b/apps/src/templates/census2017/SchoolAutocompleteDropdownWithLabel.jsx index 633a3263c14aa..50a0b91388317 100644 --- a/apps/src/templates/census2017/SchoolAutocompleteDropdownWithLabel.jsx +++ b/apps/src/templates/census2017/SchoolAutocompleteDropdownWithLabel.jsx @@ -41,6 +41,7 @@ export default class SchoolAutocompleteDropdownWithLabel extends Component { }; sendToParent = (selectValue) => { + // selectValue has a label, school, value. school has nces_id which is same as value. this.props.setField("nces", selectValue); }; diff --git a/apps/src/templates/instructions/DialogInstructions.jsx b/apps/src/templates/instructions/DialogInstructions.jsx index 75b56f175c147..d0c2baaafa15b 100644 --- a/apps/src/templates/instructions/DialogInstructions.jsx +++ b/apps/src/templates/instructions/DialogInstructions.jsx @@ -2,8 +2,6 @@ import React, {PropTypes} from 'react'; import { connect } from 'react-redux'; import Instructions from './Instructions'; import msg from '@cdo/locale'; -import processMarkdown from 'marked'; -import renderer from "../../util/StylelessRenderer"; /** * Component for displaying our instructions in the context of a modal dialog @@ -22,9 +20,6 @@ class DialogInstructions extends React.Component { }; render() { - const renderedMarkdown = this.props.longInstructions ? - processMarkdown(this.props.longInstructions, { renderer }) : undefined; - const showInstructions = !(this.props.imgOnly || this.props.hintsOnly); const showImg = !this.props.hintsOnly; return ( @@ -35,7 +30,7 @@ class DialogInstructions extends React.Component { })} shortInstructions={showInstructions ? this.props.shortInstructions : undefined} instructions2={showInstructions ? this.props.shortInstructions2 : undefined} - renderedMarkdown={showInstructions ? renderedMarkdown : undefined} + longInstructions={showInstructions ? this.props.longInstructions : undefined} imgURL={showImg ? this.props.imgURL : undefined} /> ); diff --git a/apps/src/templates/instructions/Instructions.jsx b/apps/src/templates/instructions/Instructions.jsx index 6c5d5a863d0d4..e92ffbe28228e 100644 --- a/apps/src/templates/instructions/Instructions.jsx +++ b/apps/src/templates/instructions/Instructions.jsx @@ -17,7 +17,7 @@ const styles = { * A component for displaying our level instructions text, and possibly also * authored hints UI and/or an anigif. These instructions can appear in the top * pane or in a modal dialog. In the latter case, we will sometimes show just - * the hints or just the anigif (in this case instructions/renderedMarkdown + * the hints or just the anigif (in this case instructions/longInstructions * props will be undefined). */ class Instructions extends React.Component { @@ -25,7 +25,7 @@ class Instructions extends React.Component { puzzleTitle: PropTypes.string, shortInstructions: PropTypes.string, instructions2: PropTypes.string, - renderedMarkdown: PropTypes.string, + longInstructions: PropTypes.string, imgURL: PropTypes.string, authoredHints: PropTypes.element, inputOutputTable: PropTypes.arrayOf( @@ -38,7 +38,7 @@ class Instructions extends React.Component { render() { // Body logic is as follows: // - // If we have been given rendered markdown, render a div containing + // If we have been given long instructions, render a div containing // that, optionally with inline-styled margins. We don't need to // worry about the title in this case, as it is rendered by the // Dialog header @@ -48,16 +48,16 @@ class Instructions extends React.Component { // substituteInstructionImages return (
- {this.props.renderedMarkdown && + {this.props.longInstructions && } { /* Note: In this case props.shortInstructions might be undefined, but we still want to render NonMarkdownInstructions to get the puzzle title */ - !this.props.renderedMarkdown && + !this.props.longInstructions && + > + +
); } } diff --git a/apps/src/templates/instructions/TopInstructionsCSF.jsx b/apps/src/templates/instructions/TopInstructionsCSF.jsx index 670fb83728463..848fc9a220328 100644 --- a/apps/src/templates/instructions/TopInstructionsCSF.jsx +++ b/apps/src/templates/instructions/TopInstructionsCSF.jsx @@ -1,12 +1,8 @@ -/* eslint-disable react/no-danger */ - import $ from 'jquery'; import React, {PropTypes} from 'react'; import ReactDOM from 'react-dom'; import Radium from 'radium'; -import processMarkdown from 'marked'; import classNames from 'classnames'; -import renderer from "../../util/StylelessRenderer"; import { connect } from 'react-redux'; var instructions = require('../../redux/instructions'); import { openDialog } from '../../redux/instructionsDialog'; @@ -31,6 +27,8 @@ import LegacyButton from '../LegacyButton'; import { Z_INDEX as OVERLAY_Z_INDEX } from '../Overlay'; import msg from '@cdo/locale'; +import UnsafeRenderedMarkdown from '../UnsafeRenderedMarkdown'; + import { getOuterHeight, scrollTo, @@ -606,15 +604,10 @@ class TopInstructions extends React.Component { const markdown = this.shouldDisplayShortInstructions() ? this.props.shortInstructions : this.props.longInstructions; - const renderedMarkdown = processMarkdown(markdown, { renderer }); const ttsUrl = this.shouldDisplayShortInstructions() ? this.props.ttsInstructionsUrl : this.props.ttsMarkdownInstructionsUrl; - // Only used by star wars levels - const instructions2 = this.props.shortInstructions2 ? - processMarkdown(this.props.shortInstructions2, { renderer }) : undefined; - const leftColWidth = (this.getAvatar() ? PROMPT_ICON_WIDTH : 10) + (this.props.hasAuthoredHints ? AUTHORED_HINTS_EXTRA_WIDTH : 0); @@ -660,17 +653,16 @@ class TopInstructions extends React.Component { { this.instructions = c; }} - renderedMarkdown={renderedMarkdown} + longInstructions={markdown} onResize={this.adjustMaxNeededHeight} inputOutputTable={this.props.collapsed ? undefined : this.props.inputOutputTable} imgURL={this.props.aniGifURL} inTopPane /> - {instructions2 && -
+ {this.props.shortInstructions2 && +
+ +
} {this.props.overlayVisible &&
diff --git a/apps/src/templates/instructions/TopInstructionsCSP.jsx b/apps/src/templates/instructions/TopInstructionsCSP.jsx index db2124dec8232..694af24d7e84b 100644 --- a/apps/src/templates/instructions/TopInstructionsCSP.jsx +++ b/apps/src/templates/instructions/TopInstructionsCSP.jsx @@ -4,8 +4,6 @@ import React, {PropTypes, Component} from 'react'; import ReactDOM from 'react-dom'; import Radium from 'radium'; import {connect} from 'react-redux'; -import processMarkdown from 'marked'; -import renderer from "../../util/StylelessRenderer"; import TeacherOnlyMarkdown from './TeacherOnlyMarkdown'; import FeedbacksList from "./FeedbacksList"; import TeacherFeedback from "./TeacherFeedback"; @@ -109,7 +107,7 @@ class TopInstructions extends Component { height: PropTypes.number.isRequired, expandedHeight: PropTypes.number.isRequired, maxHeight: PropTypes.number.isRequired, - markdown: PropTypes.string, + longInstructions: PropTypes.string, collapsed: PropTypes.bool.isRequired, noVisualization: PropTypes.bool.isRequired, toggleInstructionsCollapsed: PropTypes.func.isRequired, @@ -342,8 +340,7 @@ class TopInstructions extends Component {
@@ -395,7 +392,7 @@ export default connect(state => ({ expandedHeight: state.instructions.expandedHeight, maxHeight: Math.min(state.instructions.maxAvailableHeight, state.instructions.maxNeededHeight), - markdown: state.instructions.longInstructions, + longInstructions: state.instructions.longInstructions, noVisualization: state.pageConstants.noVisualization, collapsed: state.instructions.collapsed, documentationUrl: state.pageConstants.documentationUrl, diff --git a/apps/src/templates/projects/GallerySwitcher.jsx b/apps/src/templates/projects/GallerySwitcher.jsx index 487989d4c6d81..27c9dde33211b 100644 --- a/apps/src/templates/projects/GallerySwitcher.jsx +++ b/apps/src/templates/projects/GallerySwitcher.jsx @@ -58,10 +58,12 @@ class GallerySwitcher extends Component { } toggleToGallery() { + window.history.pushState(null, null, '/projects/public'); this.props.selectGallery(Galleries.PUBLIC); } toggleToMyProjects() { + window.history.pushState(null, null, '/projects'); this.props.selectGallery(Galleries.PRIVATE); } diff --git a/apps/src/templates/projects/PersonalProjectsTable.jsx b/apps/src/templates/projects/PersonalProjectsTable.jsx index 5c671c4a77a78..92336c727f79d 100644 --- a/apps/src/templates/projects/PersonalProjectsTable.jsx +++ b/apps/src/templates/projects/PersonalProjectsTable.jsx @@ -19,7 +19,6 @@ import {tableLayoutStyles, sortableOptions} from "../tables/tableConstants"; import PersonalProjectsTableActionsCell from './PersonalProjectsTableActionsCell'; import PersonalProjectsNameCell from './PersonalProjectsNameCell'; import PersonalProjectsPublishedCell from './PersonalProjectsPublishedCell'; -import firehoseClient from '@cdo/apps/lib/util/firehose'; const PROJECT_DEFAULT_IMAGE = '/blockly/media/projects/project_default.png'; @@ -314,17 +313,6 @@ class PersonalProjectsTable extends React.Component { const noProjects = this.props.personalProjectsList.length === 0; - const studyGroup = this.props.publishMethod === publishMethods.CHEVRON ? 'publish-chevron' : 'publish-button'; - - firehoseClient.putRecord( - { - study: 'project-publish', - study_group: studyGroup, - event: 'page-load', - user_id: this.props.userId, - } - ); - return (
{!noProjects && diff --git a/apps/src/templates/projects/projectConstants.js b/apps/src/templates/projects/projectConstants.js index 7534fd6a2ea02..07b64f26d41d7 100644 --- a/apps/src/templates/projects/projectConstants.js +++ b/apps/src/templates/projects/projectConstants.js @@ -76,7 +76,7 @@ export const PROJECT_TYPE_MAP = { bounce: i18n.projectTypeBounce(), flappy: i18n.projectTypeFlappy(), starwars: i18n.projectTypeStarwars(), - starwarsblocks_hour: i18n.projectTypeStarwarsBlocks(), + starwarsblocks: i18n.projectTypeStarwarsBlocks(), sports: i18n.projectTypeSports(), basketball: i18n.projectTypeBasketball(), artist_k1: i18n.projectTypeArtistPreReader(), @@ -100,7 +100,7 @@ export const FEATURED_PROJECT_TYPE_MAP = { bounce: i18n.projectTypeEvents(), flappy: i18n.projectTypeEvents(), starwars: i18n.projectTypeEvents(), - starwarsblocks_hour: i18n.projectTypeEvents(), + starwarsblocks: i18n.projectTypeEvents(), sports: i18n.projectTypeEvents(), basketball: i18n.projectTypeEvents(), artist_k1: i18n.projectTypeK1(), diff --git a/apps/src/templates/sectionAssessments/sectionAssessmentsRedux.js b/apps/src/templates/sectionAssessments/sectionAssessmentsRedux.js index cf366d37c44c7..d6b37952862d1 100644 --- a/apps/src/templates/sectionAssessments/sectionAssessmentsRedux.js +++ b/apps/src/templates/sectionAssessments/sectionAssessmentsRedux.js @@ -625,6 +625,9 @@ export const getMultipleChoiceSectionSummary = (state) => { */ export const countSubmissionsForCurrentAssessment = (state) => { const currentAssessmentId = state.sectionAssessments.assessmentId; + if (!currentAssessmentId) { + return 0; + } const isSurvey = isCurrentAssessmentSurvey(state); if (isSurvey) { const surveysStructure = state.sectionAssessments.surveysByScript[state.scriptSelection.scriptId] || {}; @@ -637,7 +640,7 @@ export const countSubmissionsForCurrentAssessment = (state) => { const studentResponses = getAssessmentResponsesForCurrentScript(state); let totalSubmissions = 0; Object.values(studentResponses).forEach((student) => { - if (Object.keys(student.responses_by_assessment).includes(currentAssessmentId.toString())) { + if (Object.keys(student.responses_by_assessment).includes(currentAssessmentId + '')) { totalSubmissions++; } }); diff --git a/apps/src/templates/sectionProgress/ScriptSelector.js b/apps/src/templates/sectionProgress/ScriptSelector.js index 0ed713e865865..3602a6f122114 100644 --- a/apps/src/templates/sectionProgress/ScriptSelector.js +++ b/apps/src/templates/sectionProgress/ScriptSelector.js @@ -45,7 +45,7 @@ export default class ScriptSelector extends Component { return (