@@ -159,10 +159,26 @@ export interface State {
159159 projectLoadTimestamp : number ;
160160 // false if we're sure the user hasn't changed the project, otherwise true
161161 projectEdited : boolean ;
162- changedHeaderExpected : boolean ;
162+ /**
163+ * Set to true when we need to update MakeCode before we open it.
164+ */
163165 appEditNeedsFlushToEditor : boolean ;
164166 isEditorOpen : boolean ;
165167 isEditorReady : boolean ;
168+ /**
169+ * Whether we're expecting an editorChange call with a new header id
170+ * because we've
171+ *
172+ * In this case we need to update app state from the project.
173+ */
174+ isEditorLoadingFile : boolean ;
175+ /**
176+ * Whether we're expecting a editorChange call with a new header id
177+ * because we've imported an updated copy of the project.
178+ *
179+ * In this case app state (e.g. dataset) will already be in sync.
180+ */
181+ isEditorImportingState : boolean ;
166182 editorStartUp : EditorStartUp ;
167183 editorStartUpTimestamp : number ;
168184 editorPromises : {
@@ -253,7 +269,8 @@ export interface Actions {
253269 editorTimedOut ( ) : void ;
254270 getEditorStartUp ( ) : EditorStartUp ;
255271 setIsEditorTimedOutDialogOpen ( isOpen : boolean ) : void ;
256- setChangedHeaderExpected ( ) : void ;
272+ setEditorLoadingFile ( ) : void ;
273+ setEditorImportingState ( ) : void ;
257274 projectFlushedToEditor ( ) : void ;
258275
259276 setDownload ( state : DownloadState ) : void ;
@@ -316,6 +333,8 @@ const createMlStore = (logging: Logging) => {
316333 model : undefined ,
317334 isEditorOpen : false ,
318335 isEditorReady : false ,
336+ isEditorLoadingFile : false ,
337+ isEditorImportingState : false ,
319338 editorStartUp : "in-progress" ,
320339 editorStartUpTimestamp : Date . now ( ) ,
321340 editorPromises : {
@@ -325,7 +344,7 @@ const createMlStore = (logging: Logging) => {
325344 isEditorTimedOutDialogOpen : false ,
326345 langChanged : false ,
327346 appEditNeedsFlushToEditor : true ,
328- changedHeaderExpected : false ,
347+ waitingForLoad : false ,
329348 // This dialog flow spans two pages
330349 trainModelDialogStage : TrainModelDialogStage . Closed ,
331350 trainModelProgress : 0 ,
@@ -669,7 +688,16 @@ const createMlStore = (logging: Logging) => {
669688 */
670689 loadProject ( project : MakeCodeProject , name : string ) {
671690 const newActions = getActionsFromProject ( project ) ;
672- set ( ( { settings } ) => {
691+ set ( ( { settings, project : prevProject } ) => {
692+ project = renameProject ( project , name ) ;
693+ project = {
694+ ...project ,
695+ header : {
696+ ...project . header ! ,
697+ // .org projects have a partial header with no id which causes MakeCode sadness
698+ id : project . header ?. id ?? prevProject . header ! . id ,
699+ } ,
700+ } ;
673701 const timestamp = Date . now ( ) ;
674702 return {
675703 settings : {
@@ -681,7 +709,7 @@ const createMlStore = (logging: Logging) => {
681709 actions : newActions ,
682710 dataWindow : getDataWindowFromActions ( newActions ) ,
683711 model : undefined ,
684- project : renameProject ( project , name ) ,
712+ project,
685713 projectEdited : true ,
686714 appEditNeedsFlushToEditor : true ,
687715 timestamp,
@@ -842,35 +870,60 @@ const createMlStore = (logging: Logging) => {
842870 } ,
843871
844872 editorChange ( newProject : MakeCodeProject ) {
873+ // Notes on past issues with the MakeCode integration:
874+ //
875+ // We update MakeCode only as needed. However, it loads the background
876+ // because we need it to be ready. This means it will have the initial
877+ // project from the state open. So we must be sure to update MakeCode
878+ // before "Edit in MakeCode" and "Save" actions.
879+ //
880+ // MakeCode has a visibility listener that will cause it to re-run
881+ // its initialization when its tab becomes hidden/visible. We aim to
882+ // ignore this when MakeCode is closed. When MakeCode is open we'll have
883+ // up-to-date state to reinit MakeCode with. In the past this has
884+ // caused us to update app state with old data from MakeCode.
885+ //
886+ // It's too slow/async from a UI perspective to rely on only
887+ // understanding project contents via editorChange as they're
888+ // delayed by MakeCode load and then the async nature of loading a
889+ // project.
890+ //
891+ // We have no choice but to write to MakeCode and wait for the project
892+ // data in editorChange when loading a hex file.
893+
845894 const actionName = "editorChange" ;
846895 set (
847896 ( state ) => {
848897 const {
849898 project : prevProject ,
850899 isEditorOpen,
851- isEditorReady ,
852- changedHeaderExpected ,
900+ isEditorImportingState ,
901+ isEditorLoadingFile ,
853902 settings,
854903 } = state ;
855904 const newProjectHeader = newProject . header ! . id ;
856905 const previousProjectHeader = prevProject . header ! . id ;
857- if ( newProjectHeader !== previousProjectHeader ) {
858- if ( changedHeaderExpected ) {
906+ if (
907+ ( isEditorLoadingFile ||
908+ isEditorOpen ||
909+ isEditorImportingState ) &&
910+ newProjectHeader !== previousProjectHeader
911+ ) {
912+ if ( isEditorImportingState ) {
913+ // It's a change but we originated it so state is in sync.
859914 logging . log (
860- `[MakeCode] Detected new project, ignoring as expected due to import . ID change: ${ prevProject . header ?. id } -> ${ newProject . header ?. id } `
915+ `[MakeCode] Ignored header change due to us syncing state . ID change: ${ prevProject . header ?. id } -> ${ newProject . header ?. id } `
861916 ) ;
862917 return {
863- changedHeaderExpected : false ,
918+ isEditorImportingState : false ,
919+ // Still need to update this for the new header id.
864920 project : newProject ,
865921 } ;
866- }
867- if ( isEditorReady ) {
922+ } else {
923+ // It's a change that originated in MakeCode, e.g. a hex load, so update our state.
868924 logging . log (
869- `[MakeCode] Detected new project, loading actions . ID change: ${ prevProject . header ?. id } -> ${ newProject . header ?. id } `
925+ `[MakeCode] Updating state from MakeCode header change . ID change: ${ prevProject . header ?. id } -> ${ newProject . header ?. id } `
870926 ) ;
871- // It's a new project. Thanks user. We'll update our state.
872- // This will cause another write to MakeCode but that's OK as it gives us
873- // a chance to validate/update the project
874927 const timestamp = Date . now ( ) ;
875928 const newActions = getActionsFromProject ( newProject ) ;
876929 return {
@@ -892,12 +945,8 @@ const createMlStore = (logging: Logging) => {
892945 dataWindow : getDataWindowFromActions ( newActions ) ,
893946 model : undefined ,
894947 isEditorOpen : false ,
948+ isEditorLoadingFile : false ,
895949 } ;
896- } else {
897- // In particular, this happens if the MakeCode init completes after we've updated our project state from an import from .org
898- logging . log (
899- `[MakeCode] Ignoring changed ID before editor ready. ID change: ${ prevProject . header ?. id } -> ${ newProject . header ?. id } `
900- ) ;
901950 }
902951 } else if ( isEditorOpen ) {
903952 logging . log (
@@ -909,6 +958,7 @@ const createMlStore = (logging: Logging) => {
909958 projectEdited : true ,
910959 } ;
911960 } else {
961+ // This lets us skip more pointless init-time edits.
912962 logging . log (
913963 `[MakeCode] Edit ignored when closed. ID ${ newProject . header ?. id } `
914964 ) ;
@@ -932,11 +982,14 @@ const createMlStore = (logging: Logging) => {
932982 setSave ( save : SaveState ) {
933983 set ( { save } , false , "setSave" ) ;
934984 } ,
935- setChangedHeaderExpected ( ) {
985+ setEditorLoadingFile ( ) {
986+ set ( { isEditorLoadingFile : true } , false , "setEditorLoadingFile" ) ;
987+ } ,
988+ setEditorImportingState ( ) {
936989 set (
937- { changedHeaderExpected : true } ,
990+ { isEditorImportingState : true } ,
938991 false ,
939- "setChangedHeaderExpected "
992+ "setEditorImportingState "
940993 ) ;
941994 } ,
942995 langChangeFlushedToEditor ( ) {
0 commit comments