Skip to content

[WIP - DO NOT MERGE] Handle opening project from microbit.org #386

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lang/ui.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Comment on lines +90 to +93
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One string added. Do not merge till strings clean up is merged

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now merged that

"coming-soon": {
"defaultMessage": "Coming soon",
"description": "Placeholder text for future projects"
Expand Down
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -96,6 +98,7 @@ const createRouter = () => {
path: createNewPageUrl(),
element: <NewPage />,
},
{ path: createImportPageUrl(), element: <ImportPage /> },
{
path: createDataSamplesPageUrl(),
element: <DataSamplesPage />,
Expand Down
1 change: 1 addition & 0 deletions src/deployment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface DeploymentConfig {

termsOfUseLink?: string;
privacyPolicyLink?: string;
activitiesBaseUrl?: string;

logging: Logging;
}
Expand Down
6 changes: 6 additions & 0 deletions src/messages/ui.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@
"value": "Close"
}
],
"code-download-error": [
{
"type": 0,
"value": "Error downloading the target code"
}
],
"coming-soon": [
{
"type": 0,
Expand Down
25 changes: 25 additions & 0 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
88 changes: 84 additions & 4 deletions src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
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";
Expand All @@ -24,8 +24,13 @@
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: [
Expand Down Expand Up @@ -63,13 +68,88 @@
],
};

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<Project> => {
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));

Check failure on line 148 in src/pages/HomePage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe argument of type `any` assigned to a parameter of type `To`

Check failure on line 148 in src/pages/HomePage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe call of an `any` typed value

Check failure on line 148 in src/pages/HomePage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .DataSamples on an `error` typed value

Check failure on line 148 in src/pages/HomePage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe argument of type `any` assigned to a parameter of type `To`

Check failure on line 148 in src/pages/HomePage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe call of an `any` typed value

Check failure on line 148 in src/pages/HomePage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .DataSamples on an `error` typed value
};
void updateAsync();
}, [activitiesBaseUrl, intl, loadProject, navigate, resource]);

return (
<DefaultPageLayout
toolbarItemsRight={
Expand Down
86 changes: 86 additions & 0 deletions src/pages/ImportPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Project } from "@microbit/makecode-embed/react";
import { useEffect } from "react";
import { IntlShape, useIntl } from "react-intl";
import { useNavigate } from "react-router";
import { useSearchParams } from "react-router-dom";
import { useDeployment } from "../deployment";
import { MicrobitOrgResource } from "../model";
import { SessionPageId } from "../pages-config";
import { useStore } from "../store";
import { createSessionPageUrl } from "../urls";

const ImportPage = () => {
const navigate = useNavigate();
const intl = useIntl();
const { activitiesBaseUrl } = useDeployment();
const resource = useMicrobitResourceSearchParams();
const loadProject = useStore((s) => s.loadProject);
console.log("resource", resource);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To remove


useEffect(() => {
const updateAsync = async () => {
if (!resource || !activitiesBaseUrl) {
return;
}
const code = await fetchMicrobitOrgResourceTargetCode(
activitiesBaseUrl,
resource,
intl
);
console.log("code", code);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To remove

loadProject(code);
navigate(createSessionPageUrl(SessionPageId.DataSamples));

Check failure on line 32 in src/pages/ImportPage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe argument of type `any` assigned to a parameter of type `To`

Check failure on line 32 in src/pages/ImportPage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe call of an `any` typed value

Check failure on line 32 in src/pages/ImportPage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .DataSamples on an `error` typed value

Check failure on line 32 in src/pages/ImportPage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe argument of type `any` assigned to a parameter of type `To`

Check failure on line 32 in src/pages/ImportPage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe call of an `any` typed value

Check failure on line 32 in src/pages/ImportPage.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .DataSamples on an `error` typed value
};
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<Project> => {
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;
27 changes: 27 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -409,6 +410,17 @@ export const useStore = create<Store>()(
});
},

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,
Expand Down Expand Up @@ -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[];
};
2 changes: 2 additions & 0 deletions src/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Loading