Skip to content

Commit a6c7eb0

Browse files
First pass at multiple projects with temporary UI
1 parent 170fa04 commit a6c7eb0

File tree

7 files changed

+456
-132
lines changed

7 files changed

+456
-132
lines changed

src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import ImportPage from "./pages/ImportPage";
5050
import NewPage from "./pages/NewPage";
5151
import TestingModelPage from "./pages/TestingModelPage";
5252
import OpenSharedProjectPage from "./pages/OpenSharedProjectPage";
53-
import { loadProjectFromStorage, useStore } from "./store";
53+
import { getAllProjects, loadProjectFromStorage, useStore } from "./store";
5454
import {
5555
createCodePageUrl,
5656
createDataSamplesPageUrl,
@@ -185,6 +185,10 @@ const createRouter = () => {
185185
{
186186
path: createNewPageUrl(),
187187
element: <NewPage />,
188+
loader: () => {
189+
const allProjectData = getAllProjects();
190+
return defer({ allProjectData });
191+
},
188192
},
189193
{ path: createImportPageUrl(), element: <ImportPage /> },
190194
{

src/ml.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,25 @@ import {
1919
import actionDataBadLabels from "./test-fixtures/shake-still-circle-legacy-bad-labels.json";
2020
import actionData from "./test-fixtures/shake-still-circle-data-samples-legacy.json";
2121
import testData from "./test-fixtures/shake-still-circle-legacy-test-data.json";
22-
import { currentDataWindow, migrateLegacyActionData } from "./project-utils";
22+
import {
23+
currentDataWindow,
24+
migrateLegacyActionDataAndAssignNewIds,
25+
} from "./project-utils";
2326

2427
const fixUpTestData = (data: Partial<OldActionData>[]): OldActionData[] => {
2528
data.forEach((action) => (action.icon = "Heart"));
2629
return data as OldActionData[];
2730
};
2831

29-
const migratedActionData = migrateLegacyActionData(fixUpTestData(actionData));
30-
const migratedActionDataBadLabels = migrateLegacyActionData(
32+
const migratedActionData = migrateLegacyActionDataAndAssignNewIds(
33+
fixUpTestData(actionData)
34+
);
35+
const migratedActionDataBadLabels = migrateLegacyActionDataAndAssignNewIds(
3136
fixUpTestData(actionDataBadLabels)
3237
);
33-
const migratedTestData = migrateLegacyActionData(fixUpTestData(testData));
38+
const migratedTestData = migrateLegacyActionDataAndAssignNewIds(
39+
fixUpTestData(testData)
40+
);
3441

3542
let trainingResult: TrainingResult;
3643
beforeAll(async () => {

src/pages/DataSamplesPage.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@ const DataSamplesPage = () => {
2626
const actions = useStore((s) => s.actions);
2727
const addNewAction = useStore((s) => s.addNewAction);
2828
const model = useStore((s) => s.model);
29+
const updateOrCreateProject = useStore((s) => s.updateOrCreateProject);
2930
const [selectedActionIdx, setSelectedActionIdx] = useState<number>(0);
3031

32+
useEffect(() => {
33+
updateOrCreateProject();
34+
}, []);
35+
3136
const navigate = useNavigate();
3237
const trainModelFlowStart = useStore((s) => s.trainModelFlowStart);
3338

src/pages/NewPage.tsx

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66
*/
77
import {
88
Box,
9+
Card,
10+
CardBody,
911
Container,
12+
Grid,
13+
GridItem,
1014
Heading,
1115
HStack,
1216
Icon,
17+
IconButton,
1318
Stack,
1419
Text,
1520
VStack,
1621
} from "@chakra-ui/react";
17-
import { ReactNode, useCallback, useRef } from "react";
22+
import { ReactNode, Suspense, useCallback, useRef, useState } from "react";
1823
import { RiAddLine, RiFolderOpenLine, RiRestartLine } from "react-icons/ri";
1924
import { FormattedMessage, useIntl } from "react-intl";
20-
import { useNavigate } from "react-router";
25+
import { Await, useAsyncValue, useLoaderData, useNavigate } from "react-router";
2126
import DefaultPageLayout, {
2227
HomeMenuItem,
2328
HomeToolbarItem,
@@ -27,16 +32,22 @@ import LoadProjectInput, {
2732
} from "../components/LoadProjectInput";
2833
import NewPageChoice from "../components/NewPageChoice";
2934
import { useLogging } from "../logging/logging-hooks";
30-
import { useStore } from "../store";
35+
import { loadProjectFromStorage, useStore } from "../store";
3136
import { createDataSamplesPageUrl } from "../urls";
3237
import { useProjectName } from "../hooks/project-hooks";
38+
import LoadingAnimation from "../components/LoadingAnimation";
39+
import { ProjectData, ProjectDataWithActions, StoreAction } from "../storage";
40+
import { DeleteIcon } from "@chakra-ui/icons";
3341

3442
const NewPage = () => {
3543
const existingSessionTimestamp = useStore((s) => s.timestamp);
3644
const projectName = useProjectName();
3745
const newSession = useStore((s) => s.newSession);
3846
const navigate = useNavigate();
3947
const logging = useLogging();
48+
const { allProjectData } = useLoaderData() as {
49+
allProjectData: ProjectData[];
50+
};
4051

4152
const handleOpenLastSession = useCallback(() => {
4253
logging.event({
@@ -165,11 +176,120 @@ const NewPage = () => {
165176
</NewPageChoice>
166177
<Box flex="1" />
167178
</HStack>
179+
<Suspense fallback={<LoadingAnimation />}>
180+
<Await resolve={allProjectData}>
181+
<ProjectsList />
182+
</Await>
183+
</Suspense>
168184
</VStack>
169185
</Container>
170186
</VStack>
171187
</DefaultPageLayout>
172188
);
173189
};
174190

191+
const ProjectsList = () => {
192+
const data = useAsyncValue() as ProjectDataWithActions[];
193+
const [projects, setProjects] = useState(data);
194+
const deleteProject = useStore((s) => s.deleteProject);
195+
196+
const handleDeleteProject = useCallback(
197+
async (id: string) => {
198+
setProjects((prev) => prev.filter((p) => p.id !== id));
199+
await deleteProject(id);
200+
},
201+
[deleteProject]
202+
);
203+
204+
return (
205+
<>
206+
<Heading as="h2" fontSize="2xl" mt={8}>
207+
Projects
208+
</Heading>
209+
<Grid mt={3} gap={3} templateColumns="repeat(5, 1fr)">
210+
{projects.map((projectData) => (
211+
<GridItem key={projectData.id} display="flex">
212+
<ProjectCard
213+
id={projectData.id}
214+
name={projectData.name}
215+
actions={projectData.actions}
216+
updatedAt={projectData.updatedAt}
217+
onDeleteProject={handleDeleteProject}
218+
/>
219+
</GridItem>
220+
))}
221+
</Grid>
222+
</>
223+
);
224+
};
225+
226+
interface ProjectCard {
227+
id: string;
228+
name: string;
229+
actions: StoreAction[];
230+
updatedAt: number;
231+
onDeleteProject: (id: string) => Promise<void>;
232+
}
233+
234+
const ProjectCard = ({
235+
id,
236+
name,
237+
actions,
238+
updatedAt,
239+
onDeleteProject,
240+
}: ProjectCard) => {
241+
const navigate = useNavigate();
242+
243+
const handleLoadProject = useCallback(
244+
async (_e: React.MouseEvent) => {
245+
await loadProjectFromStorage(id);
246+
navigate(createDataSamplesPageUrl());
247+
},
248+
[id, navigate]
249+
);
250+
251+
const handleDeleteProject = useCallback(
252+
async (e: React.MouseEvent) => {
253+
e.stopPropagation();
254+
await onDeleteProject(id);
255+
},
256+
[id, onDeleteProject]
257+
);
258+
259+
return (
260+
<Card onClick={handleLoadProject} cursor="pointer" flexGrow={1}>
261+
<IconButton
262+
aria-label="Delete project"
263+
onClick={handleDeleteProject}
264+
icon={<DeleteIcon />}
265+
position="absolute"
266+
right={1}
267+
top={1}
268+
borderRadius="sm"
269+
border="none"
270+
/>
271+
<CardBody display="flex">
272+
<Stack h="100%">
273+
<Heading as="h3" fontSize="xl">
274+
{name}
275+
</Heading>
276+
<Text mb="auto">
277+
Actions:{" "}
278+
{actions.length > 0
279+
? actions.map((a) => a.name).join(", ")
280+
: "none"}
281+
</Text>
282+
<Text>{`Last edited: ${new Intl.DateTimeFormat(undefined, {
283+
dateStyle: "medium",
284+
timeStyle: "medium",
285+
}).format(updatedAt)}`}</Text>
286+
<Text fontSize="xs" color="gray.600">
287+
id: {id}
288+
</Text>
289+
</Stack>
290+
</CardBody>
291+
</Card>
292+
);
293+
};
294+
175295
export default NewPage;

src/project-utils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const createUntitledProject = (): MakeCodeProject => ({
6767
),
6868
});
6969

70-
export const migrateLegacyActionData = (
70+
export const migrateLegacyActionDataAndAssignNewIds = (
7171
actions: OldActionData[] | ActionData[]
7272
): ActionData[] => {
7373
return actions.map((a) => {
@@ -85,6 +85,15 @@ export const migrateLegacyActionData = (
8585
createdAt: (a as OldActionData).ID,
8686
};
8787
}
88-
return a as ActionData;
88+
// Assign new unique ids to actions and recordings.
89+
// This is required if the user imports the same dataset / project twice.
90+
return {
91+
...a,
92+
id: uuid(),
93+
recordings: a.recordings.map((r) => ({
94+
...r,
95+
id: uuid(),
96+
})),
97+
} as ActionData;
8998
});
9099
};

0 commit comments

Comments
 (0)