Skip to content
Draft
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
5 changes: 4 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
createNewPageUrl,
createTestingModelPageUrl,
} from "./urls";
import { ProjectStorageProvider } from "./project-persistence/ProjectStorageProvider";

export interface ProviderLayoutProps {
children: ReactNode;
Expand Down Expand Up @@ -93,7 +94,9 @@ const Providers = ({ children }: ProviderLayoutProps) => {
<ConnectProvider {...{ usb, bluetooth, radioBridge }}>
<BufferedDataProvider>
<ConnectionStageProvider>
{children}
<ProjectStorageProvider>
{children}
</ProjectStorageProvider>
</ConnectionStageProvider>
</BufferedDataProvider>
</ConnectProvider>
Expand Down
160 changes: 84 additions & 76 deletions src/pages/NewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
* SPDX-License-Identifier: MIT
*/
import {
Box,
Container,
Grid,
Heading,
HStack,
Icon,
Stack,
Text,
VStack,
} from "@chakra-ui/react";
import { ReactNode, useCallback, useRef } from "react";
import { RiAddLine, RiFolderOpenLine, RiRestartLine } from "react-icons/ri";
import { useCallback, useRef, useState } from "react";
import { RiAddLine, RiFolderOpenLine } from "react-icons/ri";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router";
import DefaultPageLayout, {
Expand All @@ -29,39 +28,65 @@ import NewPageChoice from "../components/NewPageChoice";
import { useLogging } from "../logging/logging-hooks";
import { useStore } from "../store";
import { createDataSamplesPageUrl } from "../urls";
import { useProjectName } from "../hooks/project-hooks";
import { useStoreProjects } from "../store-persistence-hooks";
import { ProjectItem } from "../project-persistence/ProjectItem";
import ProjectHistoryModal from "../project-persistence/ProjectHistoryModal";
import { ProjectEntry } from "../project-persistence/project-list-db";
import RenameProjectModal from "../project-persistence/RenameProjectModal";
import { useProjectList } from "../project-persistence/project-list-hooks";
import { useProjectHistory } from "../project-persistence/project-history-hooks";

const NewPage = () => {
const existingSessionTimestamp = useStore((s) => s.timestamp);
const projectName = useProjectName();
const newSession = useStore((s) => s.newSession);
const navigate = useNavigate();
const logging = useLogging();
const { loadProject, newProject } = useStoreProjects();
const [showProjectHistory, setShowProjectHistory] =
useState<ProjectEntry | null>(null);
const [showProjectRename, setShowProjectRename] =
useState<ProjectEntry | null>(null);

const handleOpenLastSession = useCallback(() => {
logging.event({
type: "session-open-last",
});
navigate(createDataSamplesPageUrl());
}, [logging, navigate]);
const { projectList, deleteProject, setProjectName } = useProjectList();
const { loadRevision } = useProjectHistory();

const handleOpenSession = useCallback(
async (projectId: string) => {
logging.event({
type: "session-open-saved",
});
await loadProject(projectId);
navigate(createDataSamplesPageUrl());
},
[logging, navigate, loadProject]
);

const handleOpenRevision = useCallback(
async (projectId: string, revisionId: string) => {
logging.event({
type: "session-open-revision",
});

await loadRevision(projectId, revisionId);
navigate(createDataSamplesPageUrl());
},
[logging, navigate, loadRevision]
);

const loadProjectRef = useRef<LoadProjectInputRef>(null);
const handleContinueSessionFromFile = useCallback(() => {
loadProjectRef.current?.chooseFile();
}, []);

const handleStartNewSession = useCallback(() => {
const handleStartNewSession = useCallback(async () => {
logging.event({
type: "session-open-new",
});
await newProject();
newSession();
navigate(createDataSamplesPageUrl());
}, [logging, newSession, navigate]);
}, [logging, newSession, navigate, newProject]);

const intl = useIntl();
const lastSessionTitle = intl.formatMessage({
id: "newpage-last-session-title",
});
const continueSessionTitle = intl.formatMessage({
id: "newpage-continue-session-title",
});
Expand Down Expand Up @@ -92,47 +117,14 @@ const NewPage = () => {
flexDir={{ base: "column", lg: "row" }}
>
<NewPageChoice
onClick={handleOpenLastSession}
label={lastSessionTitle}
disabled={!existingSessionTimestamp}
icon={<Icon as={RiRestartLine} h={20} w={20} />}
onClick={handleStartNewSession}
label={newSessionTitle}
disabled={false}
icon={<Icon as={RiAddLine} h={20} w={20} />}
>
{existingSessionTimestamp ? (
<Stack mt="auto">
<Text>
<FormattedMessage
id="newpage-last-session-name"
values={{
strong: (chunks: ReactNode) => (
<Text as="span" fontWeight="bold">
{chunks}
</Text>
),
name: projectName,
}}
/>
</Text>
<Text>
<FormattedMessage
id="newpage-last-session-date"
values={{
strong: (chunks: ReactNode) => (
<Text as="span" fontWeight="bold">
{chunks}
</Text>
),
date: new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
}).format(existingSessionTimestamp),
}}
/>
</Text>
</Stack>
) : (
<Text>
<FormattedMessage id="newpage-last-session-none" />
</Text>
)}
<Text>
<FormattedMessage id="newpage-new-session-subtitle" />
</Text>
</NewPageChoice>
<NewPageChoice
onClick={handleContinueSessionFromFile}
Expand All @@ -144,30 +136,46 @@ const NewPage = () => {
</Text>
</NewPageChoice>
</HStack>

<Heading as="h2" fontSize="2xl" mt={8}>
<FormattedMessage id="newpage-section-two-title" />
Your projects
</Heading>
<HStack
alignItems="stretch"
mt={3}
gap={8}
flexDir={{ base: "column", lg: "row" }}
<Grid
position="relative"
backgroundColor="whitesmoke"
templateColumns="repeat(auto-fill, 400px)"
>
<NewPageChoice
onClick={handleStartNewSession}
label={newSessionTitle}
disabled={false}
icon={<Icon as={RiAddLine} h={20} w={20} />}
>
<Text>
<FormattedMessage id="newpage-new-session-subtitle" />
</Text>
</NewPageChoice>
<Box flex="1" />
</HStack>
{projectList?.map((proj) => (
<ProjectItem
key={proj.id}
project={proj}
loadProject={() => {
void handleOpenSession(proj.id);
}}
deleteProject={deleteProject}
renameProject={() => setShowProjectRename(proj)}
showHistory={() => setShowProjectHistory(proj)}
/>
))}
</Grid>
</VStack>
</Container>
</VStack>
<ProjectHistoryModal
isOpen={showProjectHistory !== null}
onLoadRequest={handleOpenRevision}
onDismiss={() => setShowProjectHistory(null)}
projectInfo={showProjectHistory}
/>
<RenameProjectModal
isOpen={showProjectRename !== null}
onDismiss={() => setShowProjectRename(null)}
projectInfo={showProjectRename}
handleRename={async (projectId, projectName) => {
await setProjectName(projectId, projectName);
setShowProjectRename(null);
}}
/>
</DefaultPageLayout>
);
};
Expand Down
87 changes: 87 additions & 0 deletions src/project-persistence/ProjectHistoryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, VStack, List, ListItem, Heading, Button, ModalFooter } from "@chakra-ui/react";
import { HistoryList } from "./project-history-db";
import { ProjectEntry } from "./project-list-db";
import { useCallback, useEffect, useState } from "react";
import { significantDateUnits } from "./utils";
import { useProjectHistory } from "./project-history-hooks";

interface ProjectHistoryModalProps {
onLoadRequest: (projectId: string, revisionId: string) => void;
isOpen: boolean;
onDismiss: () => void;
projectInfo: ProjectEntry | null;
}

const ProjectHistoryModal = ({
onLoadRequest,
isOpen,
onDismiss,
projectInfo,
}: ProjectHistoryModalProps) => {
const [projectHistoryList, setProjectHistoryList] =
useState<HistoryList | null>(null);
const { getHistory, saveRevision } = useProjectHistory();

const getProjectHistory = useCallback(async () => {
if (projectInfo === null) {
setProjectHistoryList(null);
return;
}
const historyList = await getHistory(projectInfo.id);
setProjectHistoryList(historyList.sort((h) => -h.timestamp));
}, [getHistory, projectInfo]);

useEffect(() => {
void getProjectHistory();
}, [projectInfo, getProjectHistory]);

return (
<Modal isOpen={isOpen && !!projectInfo} onClose={onDismiss}>
<ModalOverlay />
<ModalContent maxHeight="80dvh" width="50dvw" maxWidth="unset">
<ModalHeader>Project history</ModalHeader>
<ModalCloseButton />
<ModalBody overflowY="auto">
{projectInfo && (
<VStack>
<Heading as="h3">{projectInfo.projectName}</Heading>
<List>
<ListItem key="projectHead" fontSize="lg" pt={4}>
<Heading as="h5">Latest</Heading>
<Button
onClick={async () => {
await saveRevision(projectInfo);
await getProjectHistory();
}}
>
Save as new revision
</Button>
</ListItem>
{projectHistoryList?.map((ph) => (
<ListItem key={ph.revisionId} fontSize="lg" pt={4}>
<Heading as="h5">
Saved on {significantDateUnits(new Date(ph.timestamp))}
</Heading>
<Button
onClick={() => onLoadRequest(ph.projectId, ph.revisionId)}
>
Load as new project
</Button>
</ListItem>
))}
</List>
</VStack>
)}
</ModalBody>

<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={onDismiss}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};

export default ProjectHistoryModal;
Loading
Loading