diff --git a/lang/ui.en.json b/lang/ui.en.json
index 0e71c77e3..3cd024858 100644
--- a/lang/ui.en.json
+++ b/lang/ui.en.json
@@ -87,6 +87,10 @@
"defaultMessage": "Close",
"description": "Close button text or label"
},
+ "code-download-error": {
+ "defaultMessage": "Error downloading the target code",
+ "description": "Title for error message relating to project loading"
+ },
"coming-soon": {
"defaultMessage": "Coming soon",
"description": "Placeholder text for future projects"
diff --git a/src/App.tsx b/src/App.tsx
index 79c9b9349..b0172d7b4 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -22,11 +22,13 @@ import { LoggingProvider } from "./logging/logging-hooks";
import TranslationProvider from "./messages/TranslationProvider";
import DataSamplesPage from "./pages/DataSamplesPage";
import HomePage from "./pages/HomePage";
+import ImportPage from "./pages/ImportPage";
import NewPage from "./pages/NewPage";
import TestingModelPage from "./pages/TestingModelPage";
import {
createDataSamplesPageUrl,
createHomePageUrl,
+ createImportPageUrl,
createNewPageUrl,
createTestingModelPageUrl,
} from "./urls";
@@ -96,6 +98,7 @@ const createRouter = () => {
path: createNewPageUrl(),
element: ,
},
+ { path: createImportPageUrl(), element: },
{
path: createDataSamplesPageUrl(),
element: ,
diff --git a/src/deployment/index.ts b/src/deployment/index.ts
index ecbf7ddc7..0356d3ebd 100644
--- a/src/deployment/index.ts
+++ b/src/deployment/index.ts
@@ -56,6 +56,7 @@ export interface DeploymentConfig {
termsOfUseLink?: string;
privacyPolicyLink?: string;
+ activitiesBaseUrl?: string;
logging: Logging;
}
diff --git a/src/messages/ui.en.json b/src/messages/ui.en.json
index e35f3f044..13c2b7814 100644
--- a/src/messages/ui.en.json
+++ b/src/messages/ui.en.json
@@ -173,6 +173,12 @@
"value": "Close"
}
],
+ "code-download-error": [
+ {
+ "type": 0,
+ "value": "Error downloading the target code"
+ }
+ ],
"coming-soon": [
{
"type": 0,
diff --git a/src/model.ts b/src/model.ts
index be19a8822..b56f1c61d 100644
--- a/src/model.ts
+++ b/src/model.ts
@@ -171,3 +171,28 @@ export enum TourId {
CollectDataToTrainModel = "collectDataToTrainModel",
TestModelPage = "testModelPage",
}
+
+/**
+ * Information passed omn the URL from microbit.org.
+ * We call back into microbit.org to grab a JSON file with
+ * full details.
+ */
+export type MicrobitOrgResource = {
+ /**
+ * ID that can be used when fetching the code from microbit.org.
+ */
+ id: string;
+
+ /**
+ * Name of the microbit.org project or lesson.
+ *
+ * We use this to load the target code.
+ */
+ project: string;
+
+ /**
+ * Name of the actual code snippet.
+ * Due to a data issue this can often be the same as the project name.
+ */
+ name: string;
+};
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
index 23a2aed75..42908d8dd 100644
--- a/src/pages/HomePage.tsx
+++ b/src/pages/HomePage.tsx
@@ -10,8 +10,8 @@ import {
useInterval,
VStack,
} from "@chakra-ui/react";
-import { ReactNode, useCallback, useState } from "react";
-import { FormattedMessage, useIntl } from "react-intl";
+import { ReactNode, useCallback, useEffect, useState } from "react";
+import { FormattedMessage, IntlShape, useIntl } from "react-intl";
import { useNavigate } from "react-router";
import DefaultPageLayout from "../components/DefaultPageLayout";
import PercentageDisplay from "../components/PercentageDisplay";
@@ -24,8 +24,13 @@ import { useDeployment } from "../deployment";
import blockImage from "../images/block.png";
import xyzGraph from "../images/xyz-graph.png";
import clap from "../images/clap-square.png";
-import { createNewPageUrl } from "../urls";
+import { createNewPageUrl, createSessionPageUrl } from "../urls";
import { flags } from "../flags";
+import { useSearchParams } from "react-router-dom";
+import { MicrobitOrgResource } from "../model";
+import { Project } from "@microbit/makecode-embed/react";
+import { useStore } from "../store";
+import { SessionPageId } from "../pages-config";
const graphData = {
x: [
@@ -63,13 +68,88 @@ const graphData = {
],
};
+const useMicrobitResourceSearchParams = (): MicrobitOrgResource | undefined => {
+ const [params] = useSearchParams();
+ const id = params.get("id");
+ const project = params.get("project");
+ const name = params.get("name");
+
+ const resource: MicrobitOrgResource | undefined =
+ id && name && project
+ ? {
+ id,
+ project,
+ name,
+ }
+ : undefined;
+ return resource;
+};
+
+const isValidEditorContent = (content: Project): content is Project => {
+ return (
+ content &&
+ typeof content === "object" &&
+ "text" in content &&
+ !!content.text
+ );
+};
+
+const fetchMicrobitOrgResourceTargetCode = async (
+ activitiesBaseUrl: string,
+ resource: MicrobitOrgResource,
+ intl: IntlShape
+): Promise => {
+ const url = `${activitiesBaseUrl}${resource.id}-makecode.json`;
+ let json;
+ try {
+ const response = await fetch(url);
+ json = (await response.json()) as object;
+ } catch (e) {
+ const rethrow = new Error(
+ intl.formatMessage({ id: "code-download-error" })
+ );
+ rethrow.stack = e instanceof Error ? e.stack : undefined;
+ throw rethrow;
+ }
+ if (
+ !("editorContent" in json) ||
+ typeof json.editorContent !== "object" ||
+ !json.editorContent ||
+ !isValidEditorContent(json.editorContent)
+ ) {
+ throw new Error(intl.formatMessage({ id: "code-format-error" }));
+ }
+ return json.editorContent;
+};
+
const HomePage = () => {
const navigate = useNavigate();
const handleGetStarted = useCallback(() => {
navigate(createNewPageUrl());
}, [navigate]);
const intl = useIntl();
- const { appNameFull } = useDeployment();
+ const { appNameFull, activitiesBaseUrl } = useDeployment();
+ const resource = useMicrobitResourceSearchParams();
+ const loadProject = useStore((s) => s.loadProject);
+ console.log("resource", resource);
+
+ useEffect(() => {
+ const updateAsync = async () => {
+ if (!resource || !activitiesBaseUrl) {
+ return;
+ }
+ const code = await fetchMicrobitOrgResourceTargetCode(
+ activitiesBaseUrl,
+ resource,
+ intl
+ );
+ console.log("code", code);
+ loadProject(code);
+ navigate(createSessionPageUrl(SessionPageId.DataSamples));
+ };
+ void updateAsync();
+ }, [activitiesBaseUrl, intl, loadProject, navigate, resource]);
+
return (
{
+ const navigate = useNavigate();
+ const intl = useIntl();
+ const { activitiesBaseUrl } = useDeployment();
+ const resource = useMicrobitResourceSearchParams();
+ const loadProject = useStore((s) => s.loadProject);
+ console.log("resource", resource);
+
+ useEffect(() => {
+ const updateAsync = async () => {
+ if (!resource || !activitiesBaseUrl) {
+ return;
+ }
+ const code = await fetchMicrobitOrgResourceTargetCode(
+ activitiesBaseUrl,
+ resource,
+ intl
+ );
+ console.log("code", code);
+ loadProject(code);
+ navigate(createSessionPageUrl(SessionPageId.DataSamples));
+ };
+ void updateAsync();
+ }, [activitiesBaseUrl, intl, loadProject, navigate, resource]);
+
+ return <>>;
+};
+
+const useMicrobitResourceSearchParams = (): MicrobitOrgResource | undefined => {
+ const [params] = useSearchParams();
+ const id = params.get("id");
+ const project = params.get("project");
+ const name = params.get("name");
+
+ return id && name && project ? { id, project, name } : undefined;
+};
+
+const isValidEditorContent = (content: Project): content is Project => {
+ return (
+ content &&
+ typeof content === "object" &&
+ "text" in content &&
+ !!content.text
+ );
+};
+
+const fetchMicrobitOrgResourceTargetCode = async (
+ activitiesBaseUrl: string,
+ resource: MicrobitOrgResource,
+ intl: IntlShape
+): Promise => {
+ const url = `${activitiesBaseUrl}${resource.id}-makecode.json`;
+ let json;
+ try {
+ const response = await fetch(url);
+ json = (await response.json()) as object;
+ } catch (e) {
+ const rethrow = new Error(
+ intl.formatMessage({ id: "code-download-error" })
+ );
+ rethrow.stack = e instanceof Error ? e.stack : undefined;
+ throw rethrow;
+ }
+ if (
+ !("editorContent" in json) ||
+ typeof json.editorContent !== "object" ||
+ !json.editorContent ||
+ !isValidEditorContent(json.editorContent)
+ ) {
+ throw new Error(intl.formatMessage({ id: "code-format-error" }));
+ }
+ return json.editorContent;
+};
+
+export default ImportPage;
diff --git a/src/store.ts b/src/store.ts
index a16dcafb0..2c0c97717 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -131,6 +131,7 @@ export interface Actions {
downloadDataset(): void;
dataCollectionMicrobitConnected(): void;
loadDataset(gestures: GestureData[]): void;
+ loadProject(project: Project): void;
setEditorOpen(open: boolean): void;
recordingStarted(): void;
recordingStopped(): void;
@@ -409,6 +410,17 @@ export const useStore = create()(
});
},
+ loadProject(project: Project) {
+ set(({ projectEdited, gestures: prevGestures }) => {
+ const newGestures = getGesturesFromProject(project, prevGestures);
+ return {
+ gestures: newGestures,
+ model: undefined,
+ ...updateProject(project, projectEdited, newGestures, undefined),
+ };
+ });
+ },
+
closeTrainModelDialogs() {
set({
trainModelDialogStage: TrainModelDialogStage.Closed,
@@ -762,3 +774,18 @@ const gestureIcon = ({
}
return useableIcons[0];
};
+
+const getGesturesFromProject = (
+ project: Project,
+ prevGestures: GestureData[]
+): GestureData[] => {
+ const { text } = project;
+ if (text === undefined || !("dataset.json" in text)) {
+ return prevGestures;
+ }
+ const dataset = JSON.parse(text["dataset.json"]) as object;
+ if (typeof dataset !== "object" || !("data" in dataset)) {
+ return prevGestures;
+ }
+ return dataset.data as GestureData[];
+};
diff --git a/src/urls.ts b/src/urls.ts
index 64a4843dc..a16748e76 100644
--- a/src/urls.ts
+++ b/src/urls.ts
@@ -8,6 +8,8 @@ export const createHomePageUrl = () => `${basepath}`;
export const createNewPageUrl = () => `${basepath}new`;
+export const createImportPageUrl = () => `${basepath}import`;
+
export const createDataSamplesPageUrl = () => `${basepath}data-samples`;
export const createTestingModelPageUrl = () => `${basepath}testing-model`;