diff --git a/apps/i18n/javalab/en_us.json b/apps/i18n/javalab/en_us.json index 4a24499135a82..91eb5caa707e1 100644 --- a/apps/i18n/javalab/en_us.json +++ b/apps/i18n/javalab/en_us.json @@ -2,9 +2,11 @@ "addAComment": "Add a comment", "authorLabel": "author", "allFilesCompile": "All Files Compile!", + "backpackFileNameConflictWarning": "Saving will overwrite an existing file in your backpack", "backpackLabel": "Backpack", "backpackListLoadError": "An error occurred loading your backpack. Try again.", - "backpackSaveErrorTitle": "An error occurred", + "backpackListLoadErrorMessageCommitDialog": "Try closing and reopening this dialog.", + "backpackErrorTitle": "An error occurred", "backpackSaveErrorMessage": "Please try again", "cancel": "Cancel", "clearConsole": "Clear Console", @@ -28,6 +30,8 @@ "editor": "Editor", "emptyBackpackMessage": "Files saved to your backpack will appear here.", "enablePeerReview": "Enable Peer Review", + "errorJavabuilderConnectionGeneral": "We hit an error connecting to our server. Try again.", + "errorJavabuilderConnectionNotAuthorized": "You are not authorized to access Javalab. Do you need to log in or join a section?", "errorMediaImageLoadError": "There was an error loading the image.", "errorNeighborhoodInvalidGrid": "There was an error loading the neighborhood level. Please contact us at support@code.org, and be sure to include the URL to this page in your message.", "errorNeighborhoodInvalidDirection": "The direction provided is not a recognized. Supported directions are \"north\", \"south\", \"east\", and \"west\".", @@ -36,6 +40,7 @@ "errorNeighborhoodInvalidLocation": "The location specified isn't on the grid.", "errorNeighborhoodInvalidMove": "Painter tried to move off the grid or into an obstacle.", "errorNeighborhoodInvalidPaintLocation": "Painter tried to paint off the grid or over an obstacle.", + "errorProjectNotEditedYet": "This project has not been edited yet. The code will need to be edited before it can run.", "errorSoundInvalidAudioFileFormat": "The sound file provided is in an unsupported format. Only WAV is supported at this time.", "errorSoundMissingAudioData": "The sound file provided did not have any audio data.", "errorTheaterDuplicatePlayCommand": "The play() method can only be called once per execution of the program.", diff --git a/apps/src/applab/designElements/ColorPickerPropertyRow.jsx b/apps/src/applab/designElements/ColorPickerPropertyRow.jsx index 4fb3901c7e82e..929c8f7208c0a 100644 --- a/apps/src/applab/designElements/ColorPickerPropertyRow.jsx +++ b/apps/src/applab/designElements/ColorPickerPropertyRow.jsx @@ -24,7 +24,7 @@ export default class ColorPickerPropertyRow extends React.Component { window.removeEventListener('mousedown', this.handlePageClick); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const {initialValue} = nextProps; if (this.props.initialValue !== initialValue) { this.setState({colorPickerText: initialValue}); diff --git a/apps/src/applab/designElements/PropertyRow.jsx b/apps/src/applab/designElements/PropertyRow.jsx index ccdf8c9928d74..7f890616b2e08 100644 --- a/apps/src/applab/designElements/PropertyRow.jsx +++ b/apps/src/applab/designElements/PropertyRow.jsx @@ -31,7 +31,7 @@ export default class PropertyRow extends React.Component { isValidValue: true }; - componentWillReceiveProps(newProps) { + UNSAFE_componentWillReceiveProps(newProps) { this.setState({ value: newProps.initialValue, isValidValue: true diff --git a/apps/src/code-studio/components/Attachments.jsx b/apps/src/code-studio/components/Attachments.jsx index bb81a21a649ff..e1505bee7d9d0 100644 --- a/apps/src/code-studio/components/Attachments.jsx +++ b/apps/src/code-studio/components/Attachments.jsx @@ -17,7 +17,7 @@ export default class Attachments extends React.Component { state = {loaded: false}; - componentWillMount() { + UNSAFE_componentWillMount() { assetsApi.getFiles(this.onAssetListReceived); } diff --git a/apps/src/code-studio/components/SoundLibrary.jsx b/apps/src/code-studio/components/SoundLibrary.jsx index 217df640b5f04..82993d295f4d9 100644 --- a/apps/src/code-studio/components/SoundLibrary.jsx +++ b/apps/src/code-studio/components/SoundLibrary.jsx @@ -62,7 +62,7 @@ export default class SoundLibrary extends React.Component { selectedSound: {} }; - componentWillMount() { + UNSAFE_componentWillMount() { this.sounds = Sounds.getSingleton(); } diff --git a/apps/src/code-studio/components/SoundListEntry.jsx b/apps/src/code-studio/components/SoundListEntry.jsx index 28aa7eed18b38..c2ba6d701db0d 100644 --- a/apps/src/code-studio/components/SoundListEntry.jsx +++ b/apps/src/code-studio/components/SoundListEntry.jsx @@ -18,7 +18,7 @@ class SoundListEntry extends React.Component { state = {isPlaying: false}; - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (!nextProps.isSelected) { this.props.soundsRegistry.stopPlayingURL( this.props.soundMetadata.sourceUrl diff --git a/apps/src/code-studio/components/pairing/Pairing.jsx b/apps/src/code-studio/components/pairing/Pairing.jsx index 7207f13b6b2d9..aa9965f99ca8b 100644 --- a/apps/src/code-studio/components/pairing/Pairing.jsx +++ b/apps/src/code-studio/components/pairing/Pairing.jsx @@ -25,7 +25,7 @@ export default class Pairing extends React.Component { loading: true }; - componentWillMount() { + UNSAFE_componentWillMount() { $.ajax({ url: this.props.source, method: 'GET', diff --git a/apps/src/code-studio/components/progress/LessonLockDialog.jsx b/apps/src/code-studio/components/progress/LessonLockDialog.jsx index 853266a649d37..858abd2195624 100644 --- a/apps/src/code-studio/components/progress/LessonLockDialog.jsx +++ b/apps/src/code-studio/components/progress/LessonLockDialog.jsx @@ -33,7 +33,7 @@ class LessonLockDialog extends React.Component { }; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.saving) { return; } diff --git a/apps/src/code-studio/components/progress/teacherPanel/TeacherPanel.jsx b/apps/src/code-studio/components/progress/teacherPanel/TeacherPanel.jsx index bf01c8f20019d..23bcd5a2a5d3c 100644 --- a/apps/src/code-studio/components/progress/teacherPanel/TeacherPanel.jsx +++ b/apps/src/code-studio/components/progress/teacherPanel/TeacherPanel.jsx @@ -45,7 +45,7 @@ class TeacherPanel extends React.Component { loadLevelsWithProgress: PropTypes.func.isRequired }; - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if ( this.props.pageType !== pageTypes.scriptOverview && // no progress is shown on script overview page in teacher panel nextProps.selectedSection?.id !== this.props.selectedSection?.id diff --git a/apps/src/code-studio/pd/application/teacher/TeacherApplication.jsx b/apps/src/code-studio/pd/application/teacher/TeacherApplication.jsx index 9f53aae9da41b..97cbb18428751 100644 --- a/apps/src/code-studio/pd/application/teacher/TeacherApplication.jsx +++ b/apps/src/code-studio/pd/application/teacher/TeacherApplication.jsx @@ -25,8 +25,8 @@ export default class TeacherApplication extends FormController { /** * @override */ - componentWillMount() { - super.componentWillMount(); + UNSAFE_componentWillMount() { + super.UNSAFE_componentWillMount(); // Extract school info saved in sessionStorage, if any let reloadedSchoolId = undefined; diff --git a/apps/src/code-studio/pd/application_dashboard/application_dashboard.jsx b/apps/src/code-studio/pd/application_dashboard/application_dashboard.jsx index 801ac3443669e..6ccf4159ea94b 100644 --- a/apps/src/code-studio/pd/application_dashboard/application_dashboard.jsx +++ b/apps/src/code-studio/pd/application_dashboard/application_dashboard.jsx @@ -69,7 +69,7 @@ export default class ApplicationDashboard extends React.Component { canLockApplications: PropTypes.bool }; - componentWillMount() { + UNSAFE_componentWillMount() { store.dispatch(setRegionalPartners(this.props.regionalPartners)); store.dispatch( setRegionalPartnerFilter( diff --git a/apps/src/code-studio/pd/application_dashboard/application_loader.jsx b/apps/src/code-studio/pd/application_dashboard/application_loader.jsx index 28b341e582dfa..eeff8fd2a11e7 100644 --- a/apps/src/code-studio/pd/application_dashboard/application_loader.jsx +++ b/apps/src/code-studio/pd/application_dashboard/application_loader.jsx @@ -21,7 +21,7 @@ export default class ApplicationLoader extends React.Component { loading: true }; - componentWillMount() { + UNSAFE_componentWillMount() { this.load(); } diff --git a/apps/src/code-studio/pd/application_dashboard/cohort_view.jsx b/apps/src/code-studio/pd/application_dashboard/cohort_view.jsx index b99722a391b84..af8432b6e6779 100644 --- a/apps/src/code-studio/pd/application_dashboard/cohort_view.jsx +++ b/apps/src/code-studio/pd/application_dashboard/cohort_view.jsx @@ -34,11 +34,11 @@ class CohortView extends React.Component { applications: null }; - componentWillMount() { + UNSAFE_componentWillMount() { this.load(this.props.regionalPartnerFilter); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.regionalPartnerFilter !== nextProps.regionalPartnerFilter) { this.load(nextProps.regionalPartnerFilter); } diff --git a/apps/src/code-studio/pd/application_dashboard/cohort_view_table.jsx b/apps/src/code-studio/pd/application_dashboard/cohort_view_table.jsx index c2363ac4ad9a7..9a90f300dd369 100644 --- a/apps/src/code-studio/pd/application_dashboard/cohort_view_table.jsx +++ b/apps/src/code-studio/pd/application_dashboard/cohort_view_table.jsx @@ -55,7 +55,7 @@ export class CohortViewTable extends React.Component { }; } - componentWillUpdate() { + UNSAFE_componentWillUpdate() { this.constructColumns(); } diff --git a/apps/src/code-studio/pd/application_dashboard/detail_view_contents.jsx b/apps/src/code-studio/pd/application_dashboard/detail_view_contents.jsx index 8ba798d93592b..4f763432c932a 100644 --- a/apps/src/code-studio/pd/application_dashboard/detail_view_contents.jsx +++ b/apps/src/code-studio/pd/application_dashboard/detail_view_contents.jsx @@ -185,7 +185,7 @@ export class DetailViewContents extends React.Component { }; } - componentWillMount() { + UNSAFE_componentWillMount() { if ( this.props.applicationData.application_type === ApplicationTypes.facilitator && diff --git a/apps/src/code-studio/pd/application_dashboard/quick_view.jsx b/apps/src/code-studio/pd/application_dashboard/quick_view.jsx index 90260feb07b4c..a31748682c98d 100644 --- a/apps/src/code-studio/pd/application_dashboard/quick_view.jsx +++ b/apps/src/code-studio/pd/application_dashboard/quick_view.jsx @@ -49,13 +49,13 @@ export class QuickView extends React.Component { this.loadRequest = null; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.regionalPartnerFilter !== nextProps.regionalPartnerFilter) { this.load(nextProps.regionalPartnerFilter.value); } } - componentWillMount() { + UNSAFE_componentWillMount() { const statusList = getApplicationStatuses(this.props.route.viewType); this.statuses = Object.keys(statusList).map(v => ({ value: v, diff --git a/apps/src/code-studio/pd/application_dashboard/summary.jsx b/apps/src/code-studio/pd/application_dashboard/summary.jsx index 41d70cc60322f..70052ee7a41cc 100644 --- a/apps/src/code-studio/pd/application_dashboard/summary.jsx +++ b/apps/src/code-studio/pd/application_dashboard/summary.jsx @@ -31,13 +31,13 @@ export class Summary extends React.Component { }; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.regionalPartnerFilter !== nextProps.regionalPartnerFilter) { this.load(nextProps.regionalPartnerFilter); } } - componentWillMount() { + UNSAFE_componentWillMount() { this.load(this.props.regionalPartnerFilter); } diff --git a/apps/src/code-studio/pd/application_dashboard/workshop_assignment_loader.jsx b/apps/src/code-studio/pd/application_dashboard/workshop_assignment_loader.jsx index 34a76acb9ba28..9eaa2eacb57ea 100644 --- a/apps/src/code-studio/pd/application_dashboard/workshop_assignment_loader.jsx +++ b/apps/src/code-studio/pd/application_dashboard/workshop_assignment_loader.jsx @@ -27,7 +27,7 @@ export default class WorkshopAssignmentLoader extends React.Component { loading: true }; - componentWillMount() { + UNSAFE_componentWillMount() { this.load(); } diff --git a/apps/src/code-studio/pd/components/regional_partner_dropdown.jsx b/apps/src/code-studio/pd/components/regional_partner_dropdown.jsx index d2120fef77452..f540f97e548d0 100644 --- a/apps/src/code-studio/pd/components/regional_partner_dropdown.jsx +++ b/apps/src/code-studio/pd/components/regional_partner_dropdown.jsx @@ -58,7 +58,7 @@ export class RegionalPartnerDropdown extends React.Component { return additionalOptions; } - componentWillMount() { + UNSAFE_componentWillMount() { this.regionalPartners = this.props.regionalPartners.map(v => ({ value: v.id, label: v.name diff --git a/apps/src/code-studio/pd/components/scholarshipDropdown.jsx b/apps/src/code-studio/pd/components/scholarshipDropdown.jsx index b6d7dcafe51ff..d28acc4a93d79 100644 --- a/apps/src/code-studio/pd/components/scholarshipDropdown.jsx +++ b/apps/src/code-studio/pd/components/scholarshipDropdown.jsx @@ -3,6 +3,9 @@ import React from 'react'; import {FormGroup} from 'react-bootstrap'; import Select from 'react-select'; +// update this to lock scholarships so that scholarship status can't be updated via the UI. +const locked = true; + export class ScholarshipDropdown extends React.Component { static propTypes = { scholarshipStatus: PropTypes.string, @@ -20,7 +23,7 @@ export class ScholarshipDropdown extends React.Component { value={this.props.scholarshipStatus} onChange={this.props.onChange} options={this.props.dropdownOptions} - disabled={this.props.disabled} + disabled={locked || this.props.disabled} /> ); diff --git a/apps/src/code-studio/pd/form_components/FormController.jsx b/apps/src/code-studio/pd/form_components/FormController.jsx index ddf083c67a864..0a85aa5d89e3f 100644 --- a/apps/src/code-studio/pd/form_components/FormController.jsx +++ b/apps/src/code-studio/pd/form_components/FormController.jsx @@ -42,7 +42,7 @@ export default class FormController extends React.Component { this.onInitialize(); } - componentWillMount() { + UNSAFE_componentWillMount() { let newPage; if ( this.constructor.sessionStorageKey && @@ -63,7 +63,7 @@ export default class FormController extends React.Component { /** * @override */ - componentWillUpdate(nextProps, nextState) { + UNSAFE_componentWillUpdate(nextProps, nextState) { // If we got new errors, navigate to the first page containing errors if (this.state.errors.length === 0 && nextState.errors.length > 0) { for (let i = 0; i < this.getPageComponents().length; i++) { diff --git a/apps/src/code-studio/pd/workshop_dashboard/attendance/session_attendance.jsx b/apps/src/code-studio/pd/workshop_dashboard/attendance/session_attendance.jsx index 8e03423026a23..618d3f0ba197f 100644 --- a/apps/src/code-studio/pd/workshop_dashboard/attendance/session_attendance.jsx +++ b/apps/src/code-studio/pd/workshop_dashboard/attendance/session_attendance.jsx @@ -69,7 +69,7 @@ export class SessionAttendance extends React.Component { } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.sessionId !== this.props.sessionId) { this.load(nextProps); this.startRefreshInterval(); diff --git a/apps/src/code-studio/pd/workshop_dashboard/components/organizer_form_part.jsx b/apps/src/code-studio/pd/workshop_dashboard/components/organizer_form_part.jsx index dec233f928a84..735d586f94081 100644 --- a/apps/src/code-studio/pd/workshop_dashboard/components/organizer_form_part.jsx +++ b/apps/src/code-studio/pd/workshop_dashboard/components/organizer_form_part.jsx @@ -17,7 +17,7 @@ export default class OrganizerFormPart extends React.Component { error: false }; - componentWillMount() { + UNSAFE_componentWillMount() { this.load(this.props.workshopId); } diff --git a/apps/src/code-studio/pd/workshop_dashboard/components/workshop_form.jsx b/apps/src/code-studio/pd/workshop_dashboard/components/workshop_form.jsx index 77157a45efab0..a2f262150aed9 100644 --- a/apps/src/code-studio/pd/workshop_dashboard/components/workshop_form.jsx +++ b/apps/src/code-studio/pd/workshop_dashboard/components/workshop_form.jsx @@ -175,7 +175,7 @@ export class WorkshopForm extends React.Component { } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.readOnly && !this.props.readOnly) { this.setState(this.computeInitialState(this.props)); } diff --git a/apps/src/code-studio/pd/workshop_dashboard/components/workshop_table.jsx b/apps/src/code-studio/pd/workshop_dashboard/components/workshop_table.jsx index 1e36e90326266..10e32dc5d114f 100644 --- a/apps/src/code-studio/pd/workshop_dashboard/components/workshop_table.jsx +++ b/apps/src/code-studio/pd/workshop_dashboard/components/workshop_table.jsx @@ -46,13 +46,13 @@ export default class WorkshopTable extends React.Component { showStatus: false }; - componentWillMount() { + UNSAFE_componentWillMount() { if (this.props.onWorkshopsReceived) { this.props.onWorkshopsReceived(this.props.workshops); } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if ( !_.isEqual(this.props.workshops, nextProps.workshops) && this.props.onWorkshopsReceived diff --git a/apps/src/code-studio/pd/workshop_dashboard/components/workshop_table_loader.jsx b/apps/src/code-studio/pd/workshop_dashboard/components/workshop_table_loader.jsx index 9c5b6e2268214..d6339bb58d737 100644 --- a/apps/src/code-studio/pd/workshop_dashboard/components/workshop_table_loader.jsx +++ b/apps/src/code-studio/pd/workshop_dashboard/components/workshop_table_loader.jsx @@ -26,7 +26,7 @@ export default class WorkshopTableLoader extends React.Component { workshops: null }; - componentWillMount() { + UNSAFE_componentWillMount() { this.load = _.debounce(this.load, 200); } @@ -34,7 +34,7 @@ export default class WorkshopTableLoader extends React.Component { this.load(); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (!_.isEqual(this.props, nextProps)) { this.abortPendingRequests(); this.load(nextProps); diff --git a/apps/src/code-studio/pd/workshop_dashboard/reports/teacher_attendance_report.jsx b/apps/src/code-studio/pd/workshop_dashboard/reports/teacher_attendance_report.jsx index d9d75806a0b5a..f10d91918ef93 100644 --- a/apps/src/code-studio/pd/workshop_dashboard/reports/teacher_attendance_report.jsx +++ b/apps/src/code-studio/pd/workshop_dashboard/reports/teacher_attendance_report.jsx @@ -42,7 +42,7 @@ export class TeacherAttendanceReport extends React.Component { } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if ( nextProps.startDate !== this.props.startDate || nextProps.endDate !== this.props.endDate || diff --git a/apps/src/code-studio/pd/workshop_dashboard/reports/workshop_summary_report.jsx b/apps/src/code-studio/pd/workshop_dashboard/reports/workshop_summary_report.jsx index e3bd262def0e5..ae668e67f44d6 100644 --- a/apps/src/code-studio/pd/workshop_dashboard/reports/workshop_summary_report.jsx +++ b/apps/src/code-studio/pd/workshop_dashboard/reports/workshop_summary_report.jsx @@ -44,7 +44,7 @@ export class WorkshopSummaryReport extends React.Component { } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if ( nextProps.startDate !== this.props.startDate || nextProps.endDate !== this.props.endDate || diff --git a/apps/src/code-studio/pd/workshop_survey/VariableFormGroup.jsx b/apps/src/code-studio/pd/workshop_survey/VariableFormGroup.jsx index 59c4417dd2547..9ddfff7193379 100644 --- a/apps/src/code-studio/pd/workshop_survey/VariableFormGroup.jsx +++ b/apps/src/code-studio/pd/workshop_survey/VariableFormGroup.jsx @@ -165,7 +165,7 @@ export default class VariableFormGroup extends React.Component { this.state = {selected}; } - componentWillMount() { + UNSAFE_componentWillMount() { if (this.hasSingleSourceValue() && this.props.onChange) { // if we only have a single source value, we want to default to having it // already selected, so manually trigger an on change if we have one so diff --git a/apps/src/javalab/CommitDialog.jsx b/apps/src/javalab/CommitDialog.jsx index c0f5e093df01b..82ae6aacfbbc4 100644 --- a/apps/src/javalab/CommitDialog.jsx +++ b/apps/src/javalab/CommitDialog.jsx @@ -6,44 +6,83 @@ import StylizedBaseDialog, { FooterButton } from '@cdo/apps/componentLibrary/StylizedBaseDialog'; import {connect} from 'react-redux'; +import _ from 'lodash'; + +const fileShape = PropTypes.shape({ + name: PropTypes.string.isRequired, + commit: PropTypes.bool.isRequired, + hasConflictingName: PropTypes.bool.isRequired +}); export class UnconnectedCommitDialog extends React.Component { state = { filesToBackpack: [], + existingBackpackFiles: [], commitNotes: '', backpackSaveInProgress: false, commitSaveInProgress: false, + hasBackpackLoadError: false, hasBackpackSaveError: false, hasCommitSaveError: false }; + componentDidMount() { + this.updateBackpackFileList(); + } + + // Get updated backpack file list every time we open the modal + componentDidUpdate(prevProps) { + if (this.props.isOpen && !prevProps.isOpen) { + this.updateBackpackFileList(); + } + } + + updateBackpackFileList() { + if (this.props.backpackApi.hasBackpack()) { + this.props.backpackApi.getFileList( + () => this.setState({hasBackpackLoadError: true}), + filenames => this.setState({existingBackpackFiles: filenames}) + ); + } + } + renderFooter = () => { + const { + backpackSaveInProgress, + commitSaveInProgress, + commitNotes, + hasBackpackLoadError, + hasBackpackSaveError, + hasCommitSaveError, + filesToBackpack + } = this.state; let footerIcon = ''; let footerMessageTitle = ''; let footerMessageText = ''; let commitText = i18n.commit(); - const saveInProgress = - this.state.backpackSaveInProgress || this.state.commitSaveInProgress; - const isCommitButtonDisabled = !this.state.commitNotes || saveInProgress; - if (this.state.filesToBackpack.length > 0) { + const saveInProgress = backpackSaveInProgress || commitSaveInProgress; + const hasError = + hasBackpackSaveError || hasCommitSaveError || hasBackpackLoadError; + const isCommitButtonDisabled = + !commitNotes || saveInProgress || hasBackpackLoadError; + if (filesToBackpack.length > 0) { commitText = i18n.commitAndSave(); } // TODO: Add compile status here - if (this.state.saveInProgress) { + if (saveInProgress) { footerIcon = ( ); footerMessageTitle = i18n.saving(); - } else if ( - this.state.hasBackpackSaveError || - this.state.hasCommitSaveError - ) { + } else if (hasError) { footerIcon = ( ); - footerMessageTitle = i18n.backpackSaveErrorTitle(); - footerMessageText = i18n.backpackSaveErrorMessage(); + footerMessageTitle = i18n.backpackErrorTitle(); + footerMessageText = hasBackpackLoadError + ? i18n.backpackListLoadErrorMessageCommitDialog() + : i18n.backpackSaveErrorMessage(); } return [ @@ -79,6 +118,13 @@ export class UnconnectedCommitDialog extends React.Component { this.saveToBackpack(); }; + getConflictingBackpackFiles = () => { + return _.intersection( + this.state.filesToBackpack, + this.state.existingBackpackFiles + ); + }; + saveCommit = () => { this.setState({ hasCommitSaveError: false, @@ -115,16 +161,21 @@ export class UnconnectedCommitDialog extends React.Component { handleBackpackSaveSuccess = () => { const canClose = !this.state.commitSaveInProgress && !this.state.hasCommitSaveError; + this.setState({ hasBackpackSaveError: false, backpackSaveInProgress: false, filesToBackpack: [] }); + this.updateBackpackFileList(); + if (canClose) { this.props.handleClose(); } }; + handleBackpackLoadError = () => this.setState({hasBackpackLoadError: true}); + handleCommitSaveError = () => { this.setState({ hasCommitSaveError: true, @@ -148,6 +199,7 @@ export class UnconnectedCommitDialog extends React.Component { clearSaveStateAndClose = () => { this.setState({ hasBackpackSaveError: false, + hasBackpackLoadError: false, backpackSaveInProgress: false, hasCommitSaveError: false, commitSaveInProgress: false @@ -184,7 +236,10 @@ export class UnconnectedCommitDialog extends React.Component { ({ name, - commit: filesToBackpack.includes(name) + commit: filesToBackpack.includes(name), + hasConflictingName: this.getConflictingBackpackFiles().includes( + name + ) }))} notes={commitNotes} onToggleFile={this.toggleFileToBackpack} @@ -225,36 +280,55 @@ function CommitDialogBody({files, notes, onToggleFile, onChangeNotes}) {
{i18n.saveToBackpack()}
- {files.map(file => ( -
- - onToggleFile(file.name)} - style={styles.checkbox} + {files.map(file => { + return ( + -
- ))} + ); + })} ); } CommitDialogBody.propTypes = { - files: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string.isRequired, - commit: PropTypes.bool.isRequired - }) - ).isRequired, + files: PropTypes.arrayOf(fileShape).isRequired, notes: PropTypes.string, onToggleFile: PropTypes.func.isRequired, onChangeNotes: PropTypes.func.isRequired }; +export function CommitDialogFile({file, onToggleFile}) { + return ( +
+
+ + {file.hasConflictingName && ( +
+ {i18n.backpackFileNameConflictWarning()} +
+ )} +
+ onToggleFile(file.name)} + style={styles.checkbox} + /> +
+ ); +} + +CommitDialogFile.propTypes = { + file: fileShape.isRequired, + onToggleFile: PropTypes.func.isRequired +}; + const PADDING = 8; const styles = { bold: { @@ -281,10 +355,19 @@ const styles = { paddingBottom: PADDING / 2, borderBottom: `1px solid ${color.lightest_gray}` }, + fileLabelContainer: { + display: 'flex', + flexDirection: 'column' + }, fileLabel: { flexGrow: 2, color: color.default_text }, + fileNameConflictWarning: { + color: color.default_text, + fontStyle: 'italic', + fontSize: 12 + }, checkbox: { width: 18, height: 18 diff --git a/apps/src/javalab/JavabuilderConnection.js b/apps/src/javalab/JavabuilderConnection.js index 33bc17b6fe71e..7feb9f627fecc 100644 --- a/apps/src/javalab/JavabuilderConnection.js +++ b/apps/src/javalab/JavabuilderConnection.js @@ -31,6 +31,16 @@ export default class JavabuilderConnection { // Get the access token to connect to javabuilder and then open the websocket connection. // The token prevents access to our javabuilder AWS execution environment by un-verified users. connectJavabuilder() { + // Don't attempt to connect to Javabuilder if we do not have a project identifier. + // This typically occurs if a teacher is trying to view a student's project + // that has not been modified from the starter code. + // This case does not apply to students, who are able to execute unmodified starter code. + // See this comment for more detail: https://github.com/code-dot-org/code-dot-org/pull/42313#discussion_r701417221 + if (project.getCurrentId() === undefined) { + this.onOutputMessage(javalabMsg.errorProjectNotEditedYet()); + return; + } + $.ajax({ url: '/javabuilder/access_token', type: 'get', @@ -44,10 +54,14 @@ export default class JavabuilderConnection { }) .done(result => this.establishWebsocketConnection(result.token)) .fail(error => { - this.onOutputMessage( - 'We hit an error connecting to our server. Try again.' - ); - console.error(error.responseText); + if (error.status === 403) { + this.onOutputMessage( + javalabMsg.errorJavabuilderConnectionNotAuthorized() + ); + } else { + this.onOutputMessage(javalabMsg.errorJavabuilderConnectionGeneral()); + console.error(error.responseText); + } }); } diff --git a/apps/src/javalab/Theater.js b/apps/src/javalab/Theater.js index 9a258149b1f54..9c042f51c3b10 100644 --- a/apps/src/javalab/Theater.js +++ b/apps/src/javalab/Theater.js @@ -7,16 +7,22 @@ export default class Theater { this.context = null; this.onOutputMessage = onOutputMessage; this.onNewlineMessage = onNewlineMessage; + this.loadEventsFinished = 0; } handleSignal(data) { switch (data.value) { case TheaterSignalType.AUDIO_URL: { - this.getAudioElement().src = data.detail.url; + // Wait for the audio to load before starting playback + this.getAudioElement().src = + data.detail.url + this.getCacheBustSuffix(); + this.getAudioElement().oncanplaythrough = () => this.startPlayback(); break; } case TheaterSignalType.VISUAL_URL: { - this.getImgElement().src = data.detail.url; + // Preload the image. Once it's ready, start the playback + this.getImgElement().src = data.detail.url + this.getCacheBustSuffix(); + this.getImgElement().onload = () => this.startPlayback(); break; } default: @@ -24,8 +30,19 @@ export default class Theater { } } + startPlayback() { + this.loadEventsFinished++; + // We expect exactly 2 responses from Javabuilder. One for audio and one for video. + // Wait for both to respond and load before starting playback. + if (this.loadEventsFinished > 1) { + this.getImgElement().style.visibility = 'visible'; + this.getAudioElement().play(); + } + } + reset() { - this.getImgElement().src = ''; + this.loadEventsFinished = 0; + this.getImgElement().style.visibility = 'hidden'; this.getAudioElement().src = ''; } @@ -44,4 +61,8 @@ export default class Theater { ); this.onNewlineMessage(); } + + getCacheBustSuffix() { + return '?=' + new Date().getTime(); + } } diff --git a/apps/src/javalab/TheaterVisualizationColumn.jsx b/apps/src/javalab/TheaterVisualizationColumn.jsx index f322d4753512d..8b5038e0002ba 100644 --- a/apps/src/javalab/TheaterVisualizationColumn.jsx +++ b/apps/src/javalab/TheaterVisualizationColumn.jsx @@ -41,7 +41,7 @@ class TheaterVisualizationColumn extends React.Component {
-
@@ -57,6 +57,8 @@ const styles = { height: 800 }, theaterImage: { + // Start hidden so we can start the audio and gif at the same time. + visibility: 'hidden', width: 800, height: 800 } diff --git a/apps/src/javalab/constants.js b/apps/src/javalab/constants.js index 3a2c6bd8eb9fe..ca79630d38f80 100644 --- a/apps/src/javalab/constants.js +++ b/apps/src/javalab/constants.js @@ -18,6 +18,7 @@ export const WebSocketMessageType = { }; export const JavabuilderExceptionType = { + CLASS_NOT_FOUND: 'CLASS_NOT_FOUND', COMPILER_ERROR: 'COMPILER_ERROR', ILLEGAL_METHOD_ACCESS: 'ILLEGAL_METHOD_ACCESS', INTERNAL_COMPILER_EXCEPTION: 'INTERNAL_COMPILER_EXCEPTION', @@ -27,7 +28,7 @@ export const JavabuilderExceptionType = { NO_MAIN_METHOD: 'NO_MAIN_METHOD', RUNTIME_ERROR: 'RUNTIME_ERROR', TWO_MAIN_METHODS: 'TWO_MAIN_METHODS', - CLASS_NOT_FOUND: 'CLASS_NOT_FOUND' + UNKNOWN_ERROR: 'UNKNOWN_ERROR' }; export const NeighborhoodSignalType = { diff --git a/apps/src/javalab/javabuilderExceptionHandler.js b/apps/src/javalab/javabuilderExceptionHandler.js index 9d0662f65e168..a9a22dd182391 100644 --- a/apps/src/javalab/javabuilderExceptionHandler.js +++ b/apps/src/javalab/javabuilderExceptionHandler.js @@ -43,6 +43,7 @@ export function handleException(exceptionDetails, callback) { case JavabuilderExceptionType.INTERNAL_COMPILER_EXCEPTION: error = msg.internalCompilerException({connectionId: connectionId}); break; + case JavabuilderExceptionType.UNKNOWN_ERROR: case JavabuilderExceptionType.INTERNAL_EXCEPTION: error = msg.internalException({connectionId: connectionId}); break; diff --git a/apps/src/lib/tools/jsdebugger/JsDebugger.jsx b/apps/src/lib/tools/jsdebugger/JsDebugger.jsx index a2ee16dba13bf..24c8f3b09750b 100644 --- a/apps/src/lib/tools/jsdebugger/JsDebugger.jsx +++ b/apps/src/lib/tools/jsdebugger/JsDebugger.jsx @@ -242,7 +242,7 @@ class JsDebugger extends React.Component { this.props.onSlideOpen && this.props.onSlideOpen(this.state.openedHeight); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.isOpen && !nextProps.isOpen) { this.slideShut(); } else if (!this.props.isOpen && nextProps.isOpen) { diff --git a/apps/src/p5lab/AnimationPicker/AnimationPickerBody.jsx b/apps/src/p5lab/AnimationPicker/AnimationPickerBody.jsx index ab5e4d1bba1bb..6a6833f9e3d23 100644 --- a/apps/src/p5lab/AnimationPicker/AnimationPickerBody.jsx +++ b/apps/src/p5lab/AnimationPicker/AnimationPickerBody.jsx @@ -62,7 +62,7 @@ export default class AnimationPickerBody extends React.Component { currentPage: 0 }; - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.defaultQuery !== nextProps.defaultQuery) { const currentPage = 0; const {results, pageCount} = this.searchAssetsWrapper( diff --git a/apps/src/p5lab/AnimationPicker/AnimationPreview.jsx b/apps/src/p5lab/AnimationPicker/AnimationPreview.jsx index 350889cd31421..1dbf6352b1761 100644 --- a/apps/src/p5lab/AnimationPicker/AnimationPreview.jsx +++ b/apps/src/p5lab/AnimationPicker/AnimationPreview.jsx @@ -31,11 +31,11 @@ export default class AnimationPreview extends React.Component { extraLeftMargin: 0 }; - componentWillMount() { + UNSAFE_componentWillMount() { this.precalculateRenderProps(this.props); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { this.precalculateRenderProps(nextProps); if (nextProps.playBehavior === PlayBehavior.ALWAYS_PLAY && !this.timeout_) { this.advanceFrame(); diff --git a/apps/src/p5lab/AnimationTab/AnimationListItem.jsx b/apps/src/p5lab/AnimationTab/AnimationListItem.jsx index f27b8deb9870a..71a7d62f926d3 100644 --- a/apps/src/p5lab/AnimationTab/AnimationListItem.jsx +++ b/apps/src/p5lab/AnimationTab/AnimationListItem.jsx @@ -56,7 +56,7 @@ class AnimationListItem extends React.Component { }; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.columnWidth !== nextProps.columnWidth) { this.refs.thumbnail.forceResize(); } @@ -69,7 +69,7 @@ class AnimationListItem extends React.Component { } } - componentWillMount() { + UNSAFE_componentWillMount() { this.setState({frameDelay: this.getAnimationProps(this.props).frameDelay}); this.debouncedFrameDelay = _.debounce(() => { const latestFrameDelay = this.state.frameDelay; diff --git a/apps/src/p5lab/AnimationTab/PiskelEditor.jsx b/apps/src/p5lab/AnimationTab/PiskelEditor.jsx index 48292972bfa71..ba572e41376f6 100644 --- a/apps/src/p5lab/AnimationTab/PiskelEditor.jsx +++ b/apps/src/p5lab/AnimationTab/PiskelEditor.jsx @@ -79,7 +79,7 @@ class PiskelEditor extends React.Component { this.piskel = undefined; } - componentWillReceiveProps(newProps) { + UNSAFE_componentWillReceiveProps(newProps) { if (newProps.selectedAnimation !== this.props.selectedAnimation) { this.loadSelectedAnimation_(newProps); } diff --git a/apps/src/p5lab/P5LabVisualizationColumn.jsx b/apps/src/p5lab/P5LabVisualizationColumn.jsx index bf062fea435fd..17932ef17556c 100644 --- a/apps/src/p5lab/P5LabVisualizationColumn.jsx +++ b/apps/src/p5lab/P5LabVisualizationColumn.jsx @@ -90,7 +90,7 @@ class P5LabVisualizationColumn extends React.Component { } }; - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { // Use jQuery to turn on and off the grid since it lives in a protected div if (nextProps.showGrid !== this.props.showGrid) { if (nextProps.showGrid) { diff --git a/apps/src/p5lab/spritelab/commands/validationCommands.js b/apps/src/p5lab/spritelab/commands/validationCommands.js index 3cfd47b5fb921..e2cc28f7a6b06 100644 --- a/apps/src/p5lab/spritelab/commands/validationCommands.js +++ b/apps/src/p5lab/spritelab/commands/validationCommands.js @@ -38,6 +38,10 @@ export const commands = { return this.promptVars; }, + getSoundLog() { + return this.soundLog; + }, + getSpriteIdsInUse() { return this.getSpriteIdsInUse(); }, diff --git a/apps/src/p5lab/spritelab/commands/worldCommands.js b/apps/src/p5lab/spritelab/commands/worldCommands.js index 52dd38eee6216..cb311ae48ff24 100644 --- a/apps/src/p5lab/spritelab/commands/worldCommands.js +++ b/apps/src/p5lab/spritelab/commands/worldCommands.js @@ -36,9 +36,18 @@ export const commands = { }, playSound(url) { + this.soundLog.push(url); audioCommands.playSound({url, loop: false}); }, + playSpeech(speech) { + audioCommands.playSpeech({ + text: speech, + gender: 'female', + language: 'English' + }); + }, + printText(text) { this.printLog.push(text); getStore().dispatch(addConsoleMessage({text: text})); diff --git a/apps/src/p5lab/spritelab/libraries/CoreLibrary.js b/apps/src/p5lab/spritelab/libraries/CoreLibrary.js index 178e260ffb512..80885c27620a8 100644 --- a/apps/src/p5lab/spritelab/libraries/CoreLibrary.js +++ b/apps/src/p5lab/spritelab/libraries/CoreLibrary.js @@ -17,6 +17,7 @@ export default class CoreLibrary { this.promptVars = {}; this.eventLog = []; this.speechBubbles = []; + this.soundLog = []; this.commands = { executeDrawLoopAndCallbacks() { diff --git a/apps/src/p5lab/spritelab/libraries/PoemBotLibrary.js b/apps/src/p5lab/spritelab/libraries/PoemBotLibrary.js index 4ca020d4a7dd2..bf7a38346a607 100644 --- a/apps/src/p5lab/spritelab/libraries/PoemBotLibrary.js +++ b/apps/src/p5lab/spritelab/libraries/PoemBotLibrary.js @@ -260,20 +260,23 @@ export default class PoemBotLibrary extends CoreLibrary { } applyGlobalLineAnimation(renderInfo, frameCount) { - const progress = frameCount / POEM_DURATION; - const framesPerLine = POEM_DURATION / renderInfo.lines.length; + // Add 2 so there's time before the first line and after the last line + const framesPerLine = POEM_DURATION / (renderInfo.lines.length + 2); + const newLines = []; for (let i = 0; i < renderInfo.lines.length; i++) { + const lineNum = i + 1; // account for time before the first line shows const newLine = {...renderInfo.lines[i]}; - newLine.start = i * framesPerLine; - newLine.end = (i + 1) * framesPerLine; - newLines.push(newLine); + newLine.start = lineNum * framesPerLine; + newLine.end = (lineNum + 1) * framesPerLine; + if (this.p5.World.frameCount >= newLine.start) { + newLines.push(newLine); + } } - const numLinesToShow = Math.floor(progress * renderInfo.lines.length); return { ...renderInfo, - lines: newLines.slice(0, numLinesToShow + 1) // end index is not inclusive, so + 1 + lines: newLines }; } diff --git a/apps/src/p5lab/spritelab/poembot/commands/backgroundEffects.js b/apps/src/p5lab/spritelab/poembot/commands/backgroundEffects.js index 76216cf66ccea..c3fd1b34bce3e 100644 --- a/apps/src/p5lab/spritelab/poembot/commands/backgroundEffects.js +++ b/apps/src/p5lab/spritelab/poembot/commands/backgroundEffects.js @@ -2,6 +2,12 @@ import * as utils from './utils'; import {PALETTES} from '../constants'; export const commands = { + setBackground(color) { + this.validationInfo.backgroundEffect = color; + this.backgroundEffect = () => { + this.p5.background(color); + }; + }, // TODO: would it be possible to re-use the background/foreground effect code from dance party? setBackgroundEffect(effectName, palette) { this.validationInfo.backgroundEffect = effectName; diff --git a/apps/src/p5lab/spritelab/poembot/constants.js b/apps/src/p5lab/spritelab/poembot/constants.js index a9cd2ca434a57..ed2262b61904e 100644 --- a/apps/src/p5lab/spritelab/poembot/constants.js +++ b/apps/src/p5lab/spritelab/poembot/constants.js @@ -16,7 +16,9 @@ export const PALETTES = { summer: ['#FAD0AE', '#F69F88', '#EE6E51', '#BC4946', '#425D19', '#202E14'], autumn: ['#484F0C', '#AEA82E', '#EEBB10', '#D46324', '#731B31', '#4A173C'], winter: ['#EAECE8', '#E3DDDF', '#D3CEDC', '#A2B6BF', '#626C7D', '#A4C0D0'], - twinkling: ['#FFC702', '#FC9103', '#F17302', '#B83604', '#7E1301'] + twinkling: ['#FFC702', '#FC9103', '#F17302', '#B83604', '#7E1301'], + rainbow: ['#A800FF', '#0079FF', '#00F11D', '#FF7F00', '#FF0900'], + roses: ['#4C0606', '#86003C', '#E41F7B', '#FF8BA0 ', '#FFB6B3'] }; export const POEMS = { @@ -70,7 +72,7 @@ export const POEMS = { 'The frumious Bandersnatch!”' ] }, - rumi: { + rumi_1: { author: 'Rumi', title: 'Sing', lines: [ @@ -81,6 +83,14 @@ export const POEMS = { 'What they think.' ] }, + rumi_2: { + author: 'Rumi', + title: 'Ocean', + lines: [ + 'You are not a drop in the ocean,', + 'You are the entire ocean in one drop' + ] + }, field: { author: 'Eugene Field', title: 'Wynken, Blynken, and Nod', @@ -93,9 +103,7 @@ export const POEMS = { '"Where are you going, and what do you wish?"', 'The old moon asked the three.', '"We have come to fish for the herring-fish', - 'That live in this beautiful sea;', - 'Nets of silver and gold have we,"', - 'Said Wynken, Blynken, And Nod.' + 'That live in this beautiful sea.' ] }, twain: { diff --git a/apps/src/storage/dataBrowser/ColumnHeader.jsx b/apps/src/storage/dataBrowser/ColumnHeader.jsx index 95baa81c16da6..00181901282b9 100644 --- a/apps/src/storage/dataBrowser/ColumnHeader.jsx +++ b/apps/src/storage/dataBrowser/ColumnHeader.jsx @@ -39,7 +39,7 @@ class ColumnHeader extends React.Component { } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (!this.props.isEditing && nextProps.isEditing) { // Don't display a stale value for newName. this.setState(INITIAL_STATE); diff --git a/apps/src/storage/dataBrowser/DataTable.jsx b/apps/src/storage/dataBrowser/DataTable.jsx index 6bf9b5c693475..d7b1268b0a965 100644 --- a/apps/src/storage/dataBrowser/DataTable.jsx +++ b/apps/src/storage/dataBrowser/DataTable.jsx @@ -45,7 +45,7 @@ class DataTable extends React.Component { state = {...INITIAL_STATE}; - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { // Forget about new columns or editing columns when switching between tables. if (this.props.tableName !== nextProps.tableName) { this.setState(INITIAL_STATE); diff --git a/apps/src/storage/dataBrowser/DataTableView.jsx b/apps/src/storage/dataBrowser/DataTableView.jsx index a104859040b1c..2d22598d60509 100644 --- a/apps/src/storage/dataBrowser/DataTableView.jsx +++ b/apps/src/storage/dataBrowser/DataTableView.jsx @@ -36,7 +36,7 @@ class DataTableView extends React.Component { state = {...INITIAL_STATE}; - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { // Forget about new columns or editing columns when switching between tables. if (this.props.tableName !== nextProps.tableName) { this.setState(INITIAL_STATE); diff --git a/apps/src/storage/dataBrowser/LibraryCategory.jsx b/apps/src/storage/dataBrowser/LibraryCategory.jsx index f1a1070b1f7f0..72c228c34ca91 100644 --- a/apps/src/storage/dataBrowser/LibraryCategory.jsx +++ b/apps/src/storage/dataBrowser/LibraryCategory.jsx @@ -18,7 +18,7 @@ class LibraryCategory extends React.Component { collapsed: true }; - componentWillReceiveProps(newProps) { + UNSAFE_componentWillReceiveProps(newProps) { if ( (newProps.forceExpanded && this.state.collapsed) || (!newProps.forceExpanded && !this.state.collapsed) diff --git a/apps/src/templates/HintDisplayLightbulb.jsx b/apps/src/templates/HintDisplayLightbulb.jsx index 4be399086e472..60c7c00c2b08e 100644 --- a/apps/src/templates/HintDisplayLightbulb.jsx +++ b/apps/src/templates/HintDisplayLightbulb.jsx @@ -14,7 +14,7 @@ class HintDisplayLightbulb extends React.Component { shouldAnimate: false }; - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const receivingNewHints = nextProps.unseenHints.length > this.getCount(); this.setState({ shouldAnimate: receivingNewHints diff --git a/apps/src/templates/ImageWithStatus.jsx b/apps/src/templates/ImageWithStatus.jsx index 40953655150b2..34a4fc6978b6c 100644 --- a/apps/src/templates/ImageWithStatus.jsx +++ b/apps/src/templates/ImageWithStatus.jsx @@ -33,7 +33,7 @@ export class ImageWithStatus extends Component { }; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.src !== nextProps.src) { this.setState({imageStatus: STATUS.LOADING}); } diff --git a/apps/src/templates/ShowCodeToggle.js b/apps/src/templates/ShowCodeToggle.js index 04fcb874fe533..a8a9c2e7d178d 100644 --- a/apps/src/templates/ShowCodeToggle.js +++ b/apps/src/templates/ShowCodeToggle.js @@ -85,7 +85,7 @@ class DropletCodeToggle extends Component { this.forceUpdate(); }; - componentWillMount() { + UNSAFE_componentWillMount() { studioApp().on('afterInit', this.afterInit); } @@ -177,7 +177,7 @@ export default class ShowCodeToggle extends Component { this.forceUpdate(); }; - componentWillMount() { + UNSAFE_componentWillMount() { studioApp().on('afterInit', this.afterInit); } diff --git a/apps/src/templates/VersionHistory.jsx b/apps/src/templates/VersionHistory.jsx index b6bd01e8b47f4..6158fb3410d78 100644 --- a/apps/src/templates/VersionHistory.jsx +++ b/apps/src/templates/VersionHistory.jsx @@ -36,7 +36,7 @@ export default class VersionHistory extends React.Component { confirmingClearPuzzle: false }; - componentWillMount() { + UNSAFE_componentWillMount() { if (this.props.useFilesApi) { filesApi.getVersionHistory( this.onVersionListReceived, diff --git a/apps/src/templates/VisualizationOverlay.jsx b/apps/src/templates/VisualizationOverlay.jsx index 4ea4a018d59c4..ee87d44c38539 100644 --- a/apps/src/templates/VisualizationOverlay.jsx +++ b/apps/src/templates/VisualizationOverlay.jsx @@ -43,7 +43,7 @@ export class VisualizationOverlay extends React.Component { document.addEventListener('mousemove', this.onMouseMove); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if ( this.props.width !== nextProps.width || this.props.height !== nextProps.height diff --git a/apps/src/templates/census2017/CensusMapReplacement.jsx b/apps/src/templates/census2017/CensusMapReplacement.jsx index fdd99f7d91f76..17ecdc21c6ea4 100644 --- a/apps/src/templates/census2017/CensusMapReplacement.jsx +++ b/apps/src/templates/census2017/CensusMapReplacement.jsx @@ -390,7 +390,7 @@ export default class CensusMapReplacement extends Component { return infoWindowDom; }; - componentWillReceiveProps(newProps) { + UNSAFE_componentWillReceiveProps(newProps) { if (newProps.school !== this.props.school) { this.updateCensusMapSchool(newProps.school); } diff --git a/apps/src/templates/feedback/StudentFeedbackNotification.jsx b/apps/src/templates/feedback/StudentFeedbackNotification.jsx index b102389236715..e8d550a99acb6 100644 --- a/apps/src/templates/feedback/StudentFeedbackNotification.jsx +++ b/apps/src/templates/feedback/StudentFeedbackNotification.jsx @@ -16,7 +16,7 @@ export default class StudentFeedbackNotification extends Component { }; } - componentWillMount() { + UNSAFE_componentWillMount() { const {studentId} = this.props; $.ajax({ diff --git a/apps/src/templates/instructions/InlineAudio.jsx b/apps/src/templates/instructions/InlineAudio.jsx index 8ec171510c6f5..e3d2eeb2493a4 100644 --- a/apps/src/templates/instructions/InlineAudio.jsx +++ b/apps/src/templates/instructions/InlineAudio.jsx @@ -100,7 +100,7 @@ class InlineAudio extends React.Component { } } - componentWillUpdate(nextProps) { + UNSAFE_componentWillUpdate(nextProps) { const audioTargetWillChange = this.props.src !== nextProps.src || this.props.message !== nextProps.message; diff --git a/apps/src/templates/instructions/InstructionsCSF.jsx b/apps/src/templates/instructions/InstructionsCSF.jsx index 11613f8c52ed2..93ff2ad991e77 100644 --- a/apps/src/templates/instructions/InstructionsCSF.jsx +++ b/apps/src/templates/instructions/InstructionsCSF.jsx @@ -114,7 +114,7 @@ class InstructionsCSF extends React.Component { * the window to be super small. If we then resize it to be larger * again, we want to increase height. */ - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const minHeight = this.getMinHeight(nextProps.collapsed); const newHeight = Math.min(nextProps.maxHeight, minHeight); @@ -127,7 +127,7 @@ class InstructionsCSF extends React.Component { } } - componentWillUpdate(nextProps) { + UNSAFE_componentWillUpdate(nextProps) { const gotNewFeedback = !this.props.feedback && nextProps.feedback; if (gotNewFeedback) { this.setState({ diff --git a/apps/src/templates/instructions/InstructionsDialogWrapper.jsx b/apps/src/templates/instructions/InstructionsDialogWrapper.jsx index f90224dfe5a70..366b63e4163c0 100644 --- a/apps/src/templates/instructions/InstructionsDialogWrapper.jsx +++ b/apps/src/templates/instructions/InstructionsDialogWrapper.jsx @@ -24,7 +24,7 @@ export class UnwrappedInstructionsDialogWrapper extends React.Component { } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (!this.props.isOpen && nextProps.isOpen) { this.props.showInstructionsDialog(nextProps.autoClose); } diff --git a/apps/src/templates/instructions/TopInstructions.jsx b/apps/src/templates/instructions/TopInstructions.jsx index e3c8b4e0b034e..0fd8840f2d391 100644 --- a/apps/src/templates/instructions/TopInstructions.jsx +++ b/apps/src/templates/instructions/TopInstructions.jsx @@ -254,7 +254,7 @@ class TopInstructions extends Component { * Height can get below min height iff we resize the window to be super small. * If we then resize it to be larger again, we want to increase height. */ - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if ( !nextProps.isCollapsed && nextProps.height < MIN_HEIGHT && diff --git a/apps/src/templates/progress/ProgressLesson.jsx b/apps/src/templates/progress/ProgressLesson.jsx index a443d38aec118..b3cf4264fb45e 100644 --- a/apps/src/templates/progress/ProgressLesson.jsx +++ b/apps/src/templates/progress/ProgressLesson.jsx @@ -48,7 +48,7 @@ class ProgressLesson extends React.Component { }; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { // If we're assigned a lesson id, and it is for this lesson, uncollapse if (nextProps.currentLessonId !== this.props.currentLessonId) { this.setState({ diff --git a/apps/src/templates/projects/ProjectAppTypeArea.jsx b/apps/src/templates/projects/ProjectAppTypeArea.jsx index 30d30eee2e427..7349d5381b0ec 100644 --- a/apps/src/templates/projects/ProjectAppTypeArea.jsx +++ b/apps/src/templates/projects/ProjectAppTypeArea.jsx @@ -50,7 +50,7 @@ class ProjectAppTypeArea extends React.Component { }; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { this.setState({ maxNumProjects: nextProps.projectList ? nextProps.projectList.length : 0 }); diff --git a/apps/src/templates/projects/ProjectCardGrid.jsx b/apps/src/templates/projects/ProjectCardGrid.jsx index a5b4efa790a5a..f2e822ab49fdb 100644 --- a/apps/src/templates/projects/ProjectCardGrid.jsx +++ b/apps/src/templates/projects/ProjectCardGrid.jsx @@ -40,7 +40,7 @@ class ProjectCardGrid extends Component { limitedGallery: PropTypes.bool }; - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if ( nextProps.selectedGallery !== this.props.selectedGallery && nextProps.selectedGallery === Galleries.PUBLIC diff --git a/apps/src/templates/projects/ProjectWidgetWithData.jsx b/apps/src/templates/projects/ProjectWidgetWithData.jsx index f1d8d6e6f67b5..4861144b66095 100644 --- a/apps/src/templates/projects/ProjectWidgetWithData.jsx +++ b/apps/src/templates/projects/ProjectWidgetWithData.jsx @@ -16,7 +16,7 @@ class ProjectWidgetWithData extends React.Component { projectList: this.props.projectList || [] }; - componentWillMount() { + UNSAFE_componentWillMount() { if (this.state.projectList.length === 0) { $.ajax({ method: 'GET', diff --git a/apps/src/templates/projects/SectionProjectsList.jsx b/apps/src/templates/projects/SectionProjectsList.jsx index a8dfa5a546f6b..ade49dfaae57d 100644 --- a/apps/src/templates/projects/SectionProjectsList.jsx +++ b/apps/src/templates/projects/SectionProjectsList.jsx @@ -38,7 +38,7 @@ class SectionProjectsList extends Component { this.setState({selectedStudent}); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const studentNames = SectionProjectsList.getStudentNames( nextProps.projectsData ); diff --git a/apps/src/templates/sectionAssessments/SectionAssessments.jsx b/apps/src/templates/sectionAssessments/SectionAssessments.jsx index dc7abe6b6617a..62323f160aaaf 100644 --- a/apps/src/templates/sectionAssessments/SectionAssessments.jsx +++ b/apps/src/templates/sectionAssessments/SectionAssessments.jsx @@ -83,7 +83,7 @@ class SectionAssessments extends Component { matchDetailDialogOpen: false }; - componentWillMount() { + UNSAFE_componentWillMount() { const {scriptId, asyncLoadAssessments, sectionId} = this.props; asyncLoadAssessments(sectionId, scriptId); } diff --git a/apps/test/unit/code-studio/pd/application_dashboard/detail_view_contentsTest.js b/apps/test/unit/code-studio/pd/application_dashboard/detail_view_contentsTest.js index d5014ef358997..0d2d899bb7c77 100644 --- a/apps/test/unit/code-studio/pd/application_dashboard/detail_view_contentsTest.js +++ b/apps/test/unit/code-studio/pd/application_dashboard/detail_view_contentsTest.js @@ -358,7 +358,7 @@ describe('DetailViewContents', () => { }); }); - describe('Scholarship Teacher? row', () => { + xdescribe('Scholarship Teacher? row', () => { it('on teacher applications', () => { const detailView = mountDetailView('Teacher', { isWorkshopAdmin: true, @@ -383,12 +383,13 @@ describe('DetailViewContents', () => { .last() .simulate('click'); - // Dropdown is enabled + // Dropdown is still disabled + // note: this is the scholarship dropdown which is always disabled when scholarships are locked. expect( getLastRow() .find('Select') .prop('disabled') - ).to.equal(false); + ).to.equal(true); // Click "Save" detailView diff --git a/apps/test/unit/javalab/CommitDialogTest.js b/apps/test/unit/javalab/CommitDialogTest.js index 15b08f26dd337..82d1d4d815483 100644 --- a/apps/test/unit/javalab/CommitDialogTest.js +++ b/apps/test/unit/javalab/CommitDialogTest.js @@ -2,24 +2,46 @@ import React from 'react'; import {expect} from '../../util/reconfiguredChai'; import {shallow, mount} from 'enzyme'; import sinon from 'sinon'; -import {UnconnectedCommitDialog as CommitDialog} from '@cdo/apps/javalab/CommitDialog'; +import i18n from '@cdo/javalab/locale'; +import BackpackClientApi from '@cdo/apps/code-studio/components/backpack/BackpackClientApi'; +import { + UnconnectedCommitDialog as CommitDialog, + CommitDialogFile +} from '@cdo/apps/javalab/CommitDialog'; describe('CommitDialog test', () => { - let defaultProps, handleCommitSpy, backpackSaveFilesSpy; + let defaultProps, + handleCommitSpy, + backpackSaveFilesSpy, + backpackGetFileListStub, + hasBackpackStub; beforeEach(() => { handleCommitSpy = sinon.spy(); backpackSaveFilesSpy = sinon.spy(); + backpackGetFileListStub = sinon + .stub(BackpackClientApi.prototype, 'getFileList') + .callsArgWith(1, ['backpackFile.java']); + hasBackpackStub = sinon.stub().returns(true); + defaultProps = { isOpen: true, files: [], handleClose: () => {}, handleCommit: handleCommitSpy, sources: {}, - backpackApi: {saveFiles: backpackSaveFilesSpy} + backpackApi: { + saveFiles: backpackSaveFilesSpy, + getFileList: backpackGetFileListStub, + hasBackpack: hasBackpackStub + } }; }); + afterEach(() => { + BackpackClientApi.prototype.getFileList.restore(); + }); + it('cannot commit with message', () => { const wrapper = mount(); expect( @@ -38,6 +60,33 @@ describe('CommitDialog test', () => { ).to.be.false; }); + it('shows warning when file already in backpack included in commit', () => { + const wrapper = mount( + + ); + const file = wrapper.find(CommitDialogFile).first(); + + expect(file.text()).to.not.contain(i18n.backpackFileNameConflictWarning()); + file + .find('input[type="checkbox"]') + .first() + .simulate('change'); + expect(file.text()).to.contain(i18n.backpackFileNameConflictWarning()); + }); + + it('does not show warning when file not already in backpack included in commit', () => { + const wrapper = mount( + + ); + const file = wrapper.find(CommitDialogFile).first(); + + file + .find('input[type="checkbox"]') + .first() + .simulate('change'); + expect(file.text()).to.not.contain(i18n.backpackFileNameConflictWarning()); + }); + it('can commit and save', () => { const wrapper = shallow(); wrapper.instance().updateNotes('commit notes'); diff --git a/apps/test/unit/javalab/JavalabEditorTest.js b/apps/test/unit/javalab/JavalabEditorTest.js index 386370aa8473e..15a71f004dec1 100644 --- a/apps/test/unit/javalab/JavalabEditorTest.js +++ b/apps/test/unit/javalab/JavalabEditorTest.js @@ -16,19 +16,21 @@ import {lightMode} from '@cdo/apps/javalab/editorSetup'; import javalab, { setIsDarkMode, sourceVisibilityUpdated, - sourceValidationUpdated + sourceValidationUpdated, + setBackpackApi } from '@cdo/apps/javalab/javalabRedux'; import {setAllSources} from '../../../src/javalab/javalabRedux'; import commonReducers from '@cdo/apps/redux/commonReducers'; import {setPageConstants} from '@cdo/apps/redux/pageConstants'; import {allowConsoleWarnings} from '../../util/throwOnConsole'; +import BackpackClientApi from '@cdo/apps/code-studio/components/backpack/BackpackClientApi'; describe('Java Lab Editor Test', () => { // Warnings allowed due to usage of deprecated componentWillReceiveProps // lifecycle method. allowConsoleWarnings(); - let defaultProps, store, appOptions; + let defaultProps, store, appOptions, hasBackpackStub, backpackGetFileListStub; beforeEach(() => { stubRedux(); @@ -48,11 +50,23 @@ describe('Java Lab Editor Test', () => { isEditingStartSources: false }) ); + backpackGetFileListStub = sinon + .stub(BackpackClientApi.prototype, 'getFileList') + .callsArgWith(1, ['backpackFile.java']); + hasBackpackStub = sinon.stub().returns(true); + + store.dispatch( + setBackpackApi({ + hasBackpack: hasBackpackStub, + getFileList: backpackGetFileListStub + }) + ); }); afterEach(() => { restoreRedux(); window.appOptions = appOptions; + backpackGetFileListStub.restore(); }); const createWrapper = overrideProps => { diff --git a/apps/test/unit/javalab/TheaterTest.js b/apps/test/unit/javalab/TheaterTest.js new file mode 100644 index 0000000000000..f605ed0d919de --- /dev/null +++ b/apps/test/unit/javalab/TheaterTest.js @@ -0,0 +1,59 @@ +import sinon from 'sinon'; +import {expect} from '../../util/reconfiguredChai'; +import {TheaterSignalType} from '@cdo/apps/javalab/constants'; +import Theater from '@cdo/apps/javalab/Theater'; + +describe('Theater', () => { + let theater, playAudioSpy, imageElement, audioElement; + beforeEach(() => { + playAudioSpy = sinon.spy(); + imageElement = {}; + audioElement = {play: playAudioSpy}; + theater = new Theater(); + theater.getImgElement = () => { + return imageElement; + }; + theater.getAudioElement = () => { + return audioElement; + }; + }); + + it('sets audio detail when handleSignal with audio is called', () => { + const url = 'url'; + const data = {value: TheaterSignalType.AUDIO_URL, detail: {url: url}}; + theater.startPlayback = sinon.spy(); + theater.handleSignal(data); + expect(audioElement.src).to.contain(url); + expect(typeof audioElement.oncanplaythrough).to.equal('function'); + expect(theater.startPlayback).to.have.not.been.called; + }); + + it('sets visual detail when handleSignal with image is called', () => { + const url = 'url'; + const data = {value: TheaterSignalType.VISUAL_URL, detail: {url: url}}; + theater.startPlayback = sinon.spy(); + theater.handleSignal(data); + expect(imageElement.src).to.contain(url); + expect(typeof imageElement.onload).to.equal('function'); + expect(theater.startPlayback).to.have.not.been.called; + }); + + it('shows a/v once elements have loaded', () => { + const url = 'url'; + const audioData = { + value: TheaterSignalType.AUDIO_URL, + detail: {url: url} + }; + const visualData = { + value: TheaterSignalType.VISUAL_URL, + detail: {url: url} + }; + imageElement.style = {}; + theater.handleSignal(audioData); + theater.handleSignal(visualData); + imageElement.onload(); + audioElement.oncanplaythrough(); + expect(imageElement.style.visibility).to.equal('visible'); + expect(playAudioSpy).to.have.been.called.once; + }); +}); diff --git a/config/adhoc.yml.erb b/config/adhoc.yml.erb index 94ecc82ba92ca..ec8ab2d188149 100644 --- a/config/adhoc.yml.erb +++ b/config/adhoc.yml.erb @@ -12,3 +12,6 @@ poste_secret: not a real secret # Engineers need to be able to see raw HTTP error pages so they can debug their feature branch on an adhoc. custom_error_response: false + +javabuilder_private_key: !Secret +javabuilder_key_password: !Secret diff --git a/dashboard/app/controllers/script_levels_controller.rb b/dashboard/app/controllers/script_levels_controller.rb index b17f167ac748a..3b9668d6b09b8 100644 --- a/dashboard/app/controllers/script_levels_controller.rb +++ b/dashboard/app/controllers/script_levels_controller.rb @@ -114,10 +114,6 @@ def show raise ActiveRecord::RecordNotFound unless @script_level authorize! :read, @script_level, params.slice(:login_required) - @code_review_enabled = @script_level.level.is_a?(Javalab) && - current_user.present? && - (current_user.teacher? || current_user&.sections_as_student&.all?(&:code_review_enabled?)) - if current_user && current_user.script_level_hidden?(@script_level) view_options(full_width: true) render 'levels/_hidden_lesson' @@ -505,6 +501,10 @@ def present_level ) end + @code_review_enabled = @level.is_a?(Javalab) && + current_user.present? && + (current_user.teacher? || current_user&.sections_as_student&.all?(&:code_review_enabled?)) + view_options( full_width: true, small_footer: @game.uses_small_footer? || @level.enable_scrolling?, diff --git a/dashboard/app/models/lesson.rb b/dashboard/app/models/lesson.rb index 21ea425de98ed..9fb4bf62d32bb 100644 --- a/dashboard/app/models/lesson.rb +++ b/dashboard/app/models/lesson.rb @@ -768,7 +768,7 @@ def lesson_plan_has_verified_resources def copy_to_unit(destination_unit, new_level_suffix = nil) return if script == destination_unit raise 'Both lesson and unit must be migrated' unless script.is_migrated? && destination_unit.is_migrated? - raise 'Destination unit and lesson must be in a course version' if destination_unit.get_course_version.nil? || script.get_course_version.nil? + raise 'Destination unit and lesson must be in a course version' if destination_unit.get_course_version.nil? copied_lesson = dup copied_lesson.key = copied_lesson.name diff --git a/dashboard/app/models/lesson_group.rb b/dashboard/app/models/lesson_group.rb index 383f657c22751..e5b5f262da66c 100644 --- a/dashboard/app/models/lesson_group.rb +++ b/dashboard/app/models/lesson_group.rb @@ -228,7 +228,7 @@ def i18n_hash def copy_to_unit(destination_script, new_level_suffix = nil) return if script == destination_script raise 'Both lesson group and script must be migrated' unless script.is_migrated? && destination_script.is_migrated? - raise 'Destination script and lesson group must be in a course version' if destination_script.get_course_version.nil? || script.get_course_version.nil? + raise 'Destination script and lesson group must be in a course version' if destination_script.get_course_version.nil? copied_lesson_group = dup copied_lesson_group.script = destination_script diff --git a/dashboard/app/models/user.rb b/dashboard/app/models/user.rb index 9b823f6257ebd..75cb998f69577 100644 --- a/dashboard/app/models/user.rb +++ b/dashboard/app/models/user.rb @@ -436,7 +436,7 @@ def self.find_or_create_facilitator(params, invited_by_user) # a bit of trickery to sort most recently started/assigned/progressed scripts first and then completed has_many :user_scripts, -> {order "-completed_at asc, greatest(coalesce(started_at, 0), coalesce(assigned_at, 0), coalesce(last_progress_at, 0)) desc, user_scripts.id asc"} - has_many :scripts, -> {where(published_state: [SharedConstants::PUBLISHED_STATE.stable, SharedConstants::PUBLISHED_STATE.preview])}, through: :user_scripts, source: :script + has_many :scripts, through: :user_scripts, source: :script validates :name, presence: true, unless: -> {purged_at} validates :name, length: {within: 1..70}, allow_blank: true @@ -1569,7 +1569,7 @@ def assigned_script?(script) # Returns the set of courses the user has been assigned to or has progress in. def courses_as_student - scripts.map(&:unit_group).compact.concat(section_courses).uniq + visible_scripts.map(&:unit_group).compact.concat(section_courses).uniq end # Checks if there are any launched scripts assigned to the user. @@ -1678,6 +1678,10 @@ def section_courses all_sections.map(&:unit_group).compact.uniq end + def visible_scripts + scripts.map(&:cached).select {|s| [SharedConstants::PUBLISHED_STATE.stable, SharedConstants::PUBLISHED_STATE.preview].include?(s.get_published_state)} + end + # Figures out the unique set of scripts assigned to sections that this user # is a part of. Includes default scripts for any assigned courses as well. # @return [Array