Skip to content

Commit f001f97

Browse files
Import a JSON project like a hex file (#622)
Fixes #618
1 parent 49b495a commit f001f97

File tree

5 files changed

+84
-74
lines changed

5 files changed

+84
-74
lines changed

src/hooks/project-hooks.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ import {
2828
PostImportDialogState,
2929
SaveStep,
3030
} from "../model";
31-
import { untitledProjectName as untitled } from "../project-name";
31+
import {
32+
renameProject,
33+
untitledProjectName as untitled,
34+
} from "../project-name";
3235
import { useStore } from "../store";
3336
import {
3437
createCodePageUrl,
@@ -56,6 +59,10 @@ interface ProjectContext {
5659
project: MakeCodeProject;
5760
projectEdited: boolean;
5861
resetProject: () => void;
62+
importProject: (
63+
project: MakeCodeProject,
64+
projectNameOverride: string
65+
) => void;
5966
loadFile: (file: File, type: LoadType) => void;
6067
/**
6168
* Called to request a save.
@@ -355,6 +362,36 @@ export const ProjectProvider = ({
355362
]
356363
);
357364

365+
const importProject = useCallback(
366+
async (
367+
project: MakeCodeProject,
368+
projectNameOverride: string
369+
): Promise<void> => {
370+
const hasTimedOut = await checkIfEditorStartUpTimedOut(
371+
editorReadyPromise.promise
372+
);
373+
if (hasTimedOut) {
374+
openEditorTimedOutDialog();
375+
return;
376+
}
377+
try {
378+
// This triggers the code in editorChanged to update actions etc.
379+
await driverRef.current!.importProject({
380+
project: renameProject(project, projectNameOverride),
381+
});
382+
} catch (e) {
383+
setPostImportDialogState(PostImportDialogState.Error);
384+
}
385+
},
386+
[
387+
checkIfEditorStartUpTimedOut,
388+
driverRef,
389+
editorReadyPromise.promise,
390+
openEditorTimedOutDialog,
391+
setPostImportDialogState,
392+
]
393+
);
394+
358395
const setSave = useStore((s) => s.setSave);
359396
const save = useStore((s) => s.save);
360397
const settings = useStore((s) => s.settings);
@@ -456,6 +493,7 @@ export const ProjectProvider = ({
456493
const value = useMemo(
457494
() => ({
458495
loadFile,
496+
importProject,
459497
openEditor,
460498
browserNavigationToEditor,
461499
project,
@@ -474,6 +512,7 @@ export const ProjectProvider = ({
474512
}),
475513
[
476514
loadFile,
515+
importProject,
477516
openEditor,
478517
browserNavigationToEditor,
479518
project,

src/pages/ImportPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,21 @@ const ImportPage = () => {
7575
});
7676
}, [activitiesBaseUrl, defaultProjectName, intl, logging, params]);
7777

78-
const loadProject = useStore((s) => s.loadProject);
78+
const { importProject } = useProject();
7979
const newSession = useStore((s) => s.newSession);
8080
const timestamp = useStore((s) => s.timestamp);
8181

8282
const handleStartSession = useCallback(() => {
8383
if (project) {
84-
loadProject(project, name);
84+
importProject(project, name);
8585
navigate(createDataSamplesPageUrl());
8686
} else {
8787
// If no resource fetched, start as new empty session
8888
// with provided project name
8989
newSession(name);
9090
navigate(createDataSamplesPageUrl());
9191
}
92-
}, [loadProject, name, navigate, newSession, project]);
92+
}, [importProject, name, navigate, newSession, project]);
9393

9494
const { saveHex } = useProject();
9595
const handleSave = useCallback(() => {

src/pages/OpenSharedProjectPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,21 @@ const contentStackProps: Partial<StackProps> = {
6868
const OpenSharedProjectPage = () => {
6969
const logging = useLogging();
7070
const navigate = useNavigate();
71-
const loadProject = useStore((s) => s.loadProject);
71+
const { importProject } = useProject();
7272
const [name, setName] = useState("");
7373
const { sharedState, header, dataset, projectText } =
7474
useProjectPreload(setName);
7575
const { shortId } = useParams();
7676

7777
const handleOpenProject = useCallback(() => {
7878
if (!header || !projectText) return;
79-
loadProject({ header: { ...header, name }, text: projectText }, name);
79+
importProject({ header: { ...header, name }, text: projectText }, name);
8080
logging.event({
8181
type: "import-shared-project-complete",
8282
detail: { shortId },
8383
});
8484
navigate(createDataSamplesPageUrl());
85-
}, [loadProject, header, projectText, name, navigate, logging, shortId]);
85+
}, [importProject, header, projectText, name, navigate, logging, shortId]);
8686

8787
if (sharedState === SharedState.Failed) {
8888
return <ErrorPreloading />;

src/project-name.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { MakeCodeProject } from "@microbit/makecode-embed";
2+
import { filenames } from "./makecode/utils";
3+
14
/**
25
* (c) 2024, Micro:bit Educational Foundation and contributors
36
*
@@ -7,3 +10,26 @@ export const untitledProjectName = "Untitled";
710
export const validateProjectName = (name: string): boolean => {
811
return name.trim().length > 0;
912
};
13+
14+
export const renameProject = (
15+
project: MakeCodeProject,
16+
name: string
17+
): MakeCodeProject => {
18+
const pxtString = project.text?.[filenames.pxtJson];
19+
const pxt = JSON.parse(pxtString ?? "{}") as Record<string, unknown>;
20+
21+
return {
22+
...project,
23+
header: {
24+
...project.header!,
25+
name,
26+
},
27+
text: {
28+
...project.text,
29+
[filenames.pxtJson]: JSON.stringify({
30+
...pxt,
31+
name,
32+
}),
33+
},
34+
};
35+
};

src/store.ts

Lines changed: 12 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,38 @@ import * as tf from "@tensorflow/tfjs";
99
import { create } from "zustand";
1010
import { devtools, persist } from "zustand/middleware";
1111
import { useShallow } from "zustand/react/shallow";
12+
import { BufferedData } from "./buffered-data";
1213
import { deployment } from "./deployment";
1314
import { flags } from "./flags";
15+
import { createPromise, PromiseInfo } from "./hooks/use-promise-ref";
1416
import { Logging } from "./logging/logging";
15-
import {
16-
filenames,
17-
generateCustomFiles,
18-
generateProject,
19-
} from "./makecode/utils";
17+
import { generateCustomFiles, generateProject } from "./makecode/utils";
2018
import { Confidences, predict, trainModel } from "./ml";
19+
import { mlSettings } from "./mlConfig";
2120
import {
21+
Action,
22+
ActionData,
2223
DataSamplesView,
2324
DownloadState,
2425
DownloadStep,
25-
Action,
26-
ActionData,
26+
EditorStartUp,
2727
MicrobitToFlash,
2828
PostImportDialogState,
2929
RecordingData,
3030
SaveState,
3131
SaveStep,
32-
TourTrigger,
32+
tourSequence,
3333
TourState,
34-
TrainModelDialogStage,
35-
EditorStartUp,
34+
TourTrigger,
3635
TourTriggerName,
37-
tourSequence,
36+
TrainModelDialogStage,
3837
} from "./model";
38+
import { renameProject, untitledProjectName } from "./project-name";
3939
import { defaultSettings, Settings } from "./settings";
40+
import { getTour as getTourSpec } from "./tours";
4041
import { getTotalNumSamples } from "./utils/actions";
4142
import { defaultIcons, MakeCodeIcon } from "./utils/icons";
42-
import { untitledProjectName } from "./project-name";
43-
import { mlSettings } from "./mlConfig";
44-
import { BufferedData } from "./buffered-data";
4543
import { getDetectedAction } from "./utils/prediction";
46-
import { getTour as getTourSpec } from "./tours";
47-
import { createPromise, PromiseInfo } from "./hooks/use-promise-ref";
4844

4945
export const modelUrl = "indexeddb://micro:bit-ai-creator-model";
5046

@@ -220,7 +216,6 @@ export interface Actions {
220216
dataCollectionMicrobitConnected(): void;
221217

222218
loadDataset(actions: ActionData[]): void;
223-
loadProject(project: MakeCodeProject, name: string): void;
224219
setEditorOpen(open: boolean): void;
225220
recordingStarted(): void;
226221
recordingStopped(): void;
@@ -663,33 +658,6 @@ const createMlStore = (logging: Logging) => {
663658
});
664659
},
665660

666-
/**
667-
* Generally project loads go via MakeCode as it reads the hex but when we open projects
668-
* from microbit.org we have the JSON already and use this route.
669-
*/
670-
loadProject(project: MakeCodeProject, name: string) {
671-
const newActions = getActionsFromProject(project);
672-
set(({ settings }) => {
673-
const timestamp = Date.now();
674-
return {
675-
settings: {
676-
...settings,
677-
toursCompleted: Array.from(
678-
new Set([...settings.toursCompleted, "DataSamplesRecorded"])
679-
),
680-
},
681-
actions: newActions,
682-
dataWindow: getDataWindowFromActions(newActions),
683-
model: undefined,
684-
project: renameProject(project, name),
685-
projectEdited: true,
686-
appEditNeedsFlushToEditor: true,
687-
timestamp,
688-
// We don't update projectLoadTimestamp here as we don't want a toast notification for .org import
689-
};
690-
});
691-
},
692-
693661
closeTrainModelDialogs() {
694662
set({
695663
trainModelDialogStage: TrainModelDialogStage.Closed,
@@ -1383,26 +1351,3 @@ const getActionsFromProject = (project: MakeCodeProject): ActionData[] => {
13831351
}
13841352
return dataset.data as ActionData[];
13851353
};
1386-
1387-
const renameProject = (
1388-
project: MakeCodeProject,
1389-
name: string
1390-
): MakeCodeProject => {
1391-
const pxtString = project.text?.[filenames.pxtJson];
1392-
const pxt = JSON.parse(pxtString ?? "{}") as Record<string, unknown>;
1393-
1394-
return {
1395-
...project,
1396-
header: {
1397-
...project.header!,
1398-
name,
1399-
},
1400-
text: {
1401-
...project.text,
1402-
[filenames.pxtJson]: JSON.stringify({
1403-
...pxt,
1404-
name,
1405-
}),
1406-
},
1407-
};
1408-
};

0 commit comments

Comments
 (0)