From d0d41777b55b349cec3522c54112d8dc37889e09 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:48:28 -0800 Subject: [PATCH 01/19] chore: added @comapeo/core-react --- package-lock.json | 15 +++++++++++++++ package.json | 1 + 2 files changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9b65cab..400df08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "valibot": "^0.42.1" }, "devDependencies": { + "@comapeo/core-react": "0.1.0", "@electron-forge/cli": "^7.5.0", "@electron-forge/maker-deb": "^7.5.0", "@electron-forge/maker-dmg": "^7.5.0", @@ -502,6 +503,20 @@ "yauzl-promise": "^4.0.0" } }, + "node_modules/@comapeo/core-react": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@comapeo/core-react/-/core-react-0.1.0.tgz", + "integrity": "sha512-mnUs0Fk1eQ4tgh6QqXQQcVWjBGPA/Y6Ap+Hw3PqLqmMFIYuxoe5AGGohWtrh/zXtPLkio+HTroEmhivJI3qkWQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@comapeo/core": "*", + "@comapeo/ipc": "*", + "@comapeo/schema": "*", + "@tanstack/react-query": "^5 || ^6", + "react": "^18 || ^19" + } + }, "node_modules/@comapeo/fallback-smp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@comapeo/fallback-smp/-/fallback-smp-1.0.0.tgz", diff --git a/package.json b/package.json index d2f9620..9a5b2a4 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "valibot": "^0.42.1" }, "devDependencies": { + "@comapeo/core-react": "0.1.0", "@electron-forge/cli": "^7.5.0", "@electron-forge/maker-deb": "^7.5.0", "@electron-forge/maker-dmg": "^7.5.0", From 4d7113efdf721ddc12f242a30ade4a1f9b666380 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:08:16 -0800 Subject: [PATCH 02/19] chore: update dependency to latest --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 400df08..3fd588f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "valibot": "^0.42.1" }, "devDependencies": { - "@comapeo/core-react": "0.1.0", + "@comapeo/core-react": "0.1.1", "@electron-forge/cli": "^7.5.0", "@electron-forge/maker-deb": "^7.5.0", "@electron-forge/maker-dmg": "^7.5.0", @@ -504,16 +504,16 @@ } }, "node_modules/@comapeo/core-react": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@comapeo/core-react/-/core-react-0.1.0.tgz", - "integrity": "sha512-mnUs0Fk1eQ4tgh6QqXQQcVWjBGPA/Y6Ap+Hw3PqLqmMFIYuxoe5AGGohWtrh/zXtPLkio+HTroEmhivJI3qkWQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@comapeo/core-react/-/core-react-0.1.1.tgz", + "integrity": "sha512-X39PjcMpQdTURa7oQrB/KIva6Lvv5D4Um13Pag+3moNOI7xJ3vWdL440Hg1bcH9hM/zvwTvHHOed0rVO1vn3sw==", "dev": true, "license": "MIT", "peerDependencies": { "@comapeo/core": "*", "@comapeo/ipc": "*", "@comapeo/schema": "*", - "@tanstack/react-query": "^5 || ^6", + "@tanstack/react-query": "^5", "react": "^18 || ^19" } }, diff --git a/package.json b/package.json index 9a5b2a4..3a22813 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "valibot": "^0.42.1" }, "devDependencies": { - "@comapeo/core-react": "0.1.0", + "@comapeo/core-react": "0.1.1", "@electron-forge/cli": "^7.5.0", "@electron-forge/maker-deb": "^7.5.0", "@electron-forge/maker-dmg": "^7.5.0", From 1dbb268b557aa0bafb659977599aafd5b689ed9f Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:08:54 -0800 Subject: [PATCH 03/19] chore: update Api context provider --- src/renderer/src/contexts/ApiContext.tsx | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/renderer/src/contexts/ApiContext.tsx b/src/renderer/src/contexts/ApiContext.tsx index 6e3ef99..65cb3fe 100644 --- a/src/renderer/src/contexts/ApiContext.tsx +++ b/src/renderer/src/contexts/ApiContext.tsx @@ -1,14 +1,7 @@ -import { - createContext, - useContext, - useEffect, - useState, - type PropsWithChildren, -} from 'react' +import { useEffect, useState, type PropsWithChildren } from 'react' +import { ClientApiProvider } from '@comapeo/core-react' import { createMapeoClient, type MapeoClientApi } from '@comapeo/ipc' -const ApiContext = createContext(null) - export function ApiProvider({ children }: PropsWithChildren) { const [api, setApi] = useState(null) @@ -44,13 +37,5 @@ export function ApiProvider({ children }: PropsWithChildren) { if (!api) return null - return {children} -} - -export function useApi() { - const api = useContext(ApiContext) - - if (!api) throw new Error('MapeoApiContext provider needs to be set up') - - return api + return {children} } From a1010102608d817a32b67094e3bd8ac66c538b6b Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:12:14 -0800 Subject: [PATCH 04/19] chore: update queries --- src/renderer/src/queries/deviceInfo.ts | 24 +++++------------------- src/renderer/src/queries/projects.ts | 23 ++++------------------- src/renderer/src/routes/index.tsx | 5 ++--- 3 files changed, 11 insertions(+), 41 deletions(-) diff --git a/src/renderer/src/queries/deviceInfo.ts b/src/renderer/src/queries/deviceInfo.ts index 034b7f6..a1688df 100644 --- a/src/renderer/src/queries/deviceInfo.ts +++ b/src/renderer/src/queries/deviceInfo.ts @@ -1,34 +1,20 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' - -import { useApi } from '../contexts/ApiContext' - -export const DEVICE_INFO_KEY = 'deviceInfo' - -export const useDeviceInfo = () => { - const api = useApi() - - return useQuery({ - queryKey: [DEVICE_INFO_KEY], - queryFn: async () => { - return await api.getDeviceInfo() - }, - }) -} +import { getDeviceInfoQueryKey, useClientApi } from '@comapeo/core-react' +import { useMutation, useQueryClient } from '@tanstack/react-query' export const useEditDeviceInfo = () => { - const api = useApi() + const clientApi = useClientApi() const queryClient = useQueryClient() return useMutation({ mutationKey: ['device'], mutationFn: async (name: string) => { - return api.setDeviceInfo({ + return clientApi.setDeviceInfo({ name, deviceType: 'desktop', }) }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [DEVICE_INFO_KEY] }) + queryClient.invalidateQueries({ queryKey: getDeviceInfoQueryKey() }) }, }) } diff --git a/src/renderer/src/queries/projects.ts b/src/renderer/src/queries/projects.ts index fb0c0de..cf59b31 100644 --- a/src/renderer/src/queries/projects.ts +++ b/src/renderer/src/queries/projects.ts @@ -1,25 +1,10 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { getProjectsQueryKey, useClientApi } from '@comapeo/core-react' +import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useApi } from '../contexts/ApiContext' - -export const PROJECT_SETTINGS_KEY = 'project_settings' export const CREATE_PROJECT_KEY = 'create_project' -export const PROJECTS_KEY = 'projects' -export const PROJECT_MEMBERS_KEY = 'project_members' - -export function useAllProjects() { - const api = useApi() - - return useQuery({ - queryKey: [PROJECTS_KEY], - queryFn: () => { - return api.listProjects() - }, - }) -} export function useCreateProject() { - const api = useApi() + const api = useClientApi() const queryClient = useQueryClient() return useMutation({ @@ -29,7 +14,7 @@ export function useCreateProject() { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [PROJECTS_KEY], + queryKey: getProjectsQueryKey(), }) }, }) diff --git a/src/renderer/src/routes/index.tsx b/src/renderer/src/routes/index.tsx index a06e285..c0e6b89 100644 --- a/src/renderer/src/routes/index.tsx +++ b/src/renderer/src/routes/index.tsx @@ -1,17 +1,16 @@ import * as React from 'react' +import { useOwnDeviceInfo } from '@comapeo/core-react' import Box from '@mui/material/Box' import CircularProgress from '@mui/material/CircularProgress' import { createFileRoute, useRouter } from '@tanstack/react-router' -import { useDeviceInfo } from '../queries/deviceInfo' - export const Route = createFileRoute('/')({ component: RouteComponent, }) function RouteComponent() { const router = useRouter() - const { data } = useDeviceInfo() + const { data } = useOwnDeviceInfo() const hasCreatedDeviceName = data?.name !== undefined React.useEffect(() => { From 2cc482de71958eba04dd0962e8f51ae9f50875d2 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:13:09 -0800 Subject: [PATCH 05/19] chore: change folder name to mutations --- src/renderer/src/{queries => mutations}/deviceInfo.ts | 0 src/renderer/src/{queries => mutations}/projects.ts | 0 src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx | 2 +- src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename src/renderer/src/{queries => mutations}/deviceInfo.ts (100%) rename src/renderer/src/{queries => mutations}/projects.ts (100%) diff --git a/src/renderer/src/queries/deviceInfo.ts b/src/renderer/src/mutations/deviceInfo.ts similarity index 100% rename from src/renderer/src/queries/deviceInfo.ts rename to src/renderer/src/mutations/deviceInfo.ts diff --git a/src/renderer/src/queries/projects.ts b/src/renderer/src/mutations/projects.ts similarity index 100% rename from src/renderer/src/queries/projects.ts rename to src/renderer/src/mutations/projects.ts diff --git a/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx b/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx index 6c944a6..d8f1470 100644 --- a/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx +++ b/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx @@ -21,7 +21,7 @@ import { import { Text } from '../../components/Text' import { PROJECT_NAME_MAX_LENGTH_GRAPHEMES } from '../../constants' import ProjectImage from '../../images/add_square.png' -import { useCreateProject } from '../../queries/projects' +import { useCreateProject } from '../../mutations/projects' export const m = defineMessages({ title: { diff --git a/src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx b/src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx index c749ed7..0a524d7 100644 --- a/src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx +++ b/src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx @@ -15,7 +15,7 @@ import { import { Text } from '../../components/Text' import { DEVICE_NAME_MAX_LENGTH_GRAPHEMES } from '../../constants' import DeviceImage from '../../images/device.png' -import { useEditDeviceInfo } from '../../queries/deviceInfo' +import { useEditDeviceInfo } from '../../mutations/deviceInfo' export const m = defineMessages({ title: { From e27a91777bfc9b8ecc7322378efc48a8d5bffe22 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:13:09 -0800 Subject: [PATCH 06/19] chore: established createPersistedStore --- package-lock.json | 33 ++++++++++++++++++- package.json | 3 +- .../persistedState/createPersistedState.ts | 25 ++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/hooks/persistedState/createPersistedState.ts diff --git a/package-lock.json b/package-lock.json index 3fd588f..313225a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,8 @@ "typescript-eslint": "^8.15.0", "vite": "^5.4.11", "vite-plugin-svgr": "^4.3.0", - "vitest": "2.1.8" + "vitest": "2.1.8", + "zustand": "5.0.2" } }, "node_modules/@ampproject/remapping": { @@ -18572,6 +18573,36 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz", + "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 3a22813..89cd396 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,8 @@ "typescript-eslint": "^8.15.0", "vite": "^5.4.11", "vite-plugin-svgr": "^4.3.0", - "vitest": "2.1.8" + "vitest": "2.1.8", + "zustand": "5.0.2" }, "overrides": { "better-sqlite3": "11.5.0" diff --git a/src/renderer/src/hooks/persistedState/createPersistedState.ts b/src/renderer/src/hooks/persistedState/createPersistedState.ts new file mode 100644 index 0000000..2054b43 --- /dev/null +++ b/src/renderer/src/hooks/persistedState/createPersistedState.ts @@ -0,0 +1,25 @@ +import { createStore, type StateCreator } from 'zustand' +import { persist } from 'zustand/middleware' + +type PersistedStoreKey = 'ActiveProjectId' + +export function createPersistedStore( + ...args: Parameters> +) { + const store = createStore()(createPersistMiddleware(...args)) + store.setState((state) => ({ + ...state, + ...args[0], + })) + + return store +} + +function createPersistMiddleware( + slice: StateCreator, + persistedStoreKey: PersistedStoreKey, +) { + return persist(slice, { + name: persistedStoreKey, + }) +} From 8360a18899613c8497b9db8d4d6bbb664eea4fee Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:15:04 -0800 Subject: [PATCH 07/19] chore: moved mutations into hooks --- src/renderer/src/{ => hooks}/mutations/deviceInfo.ts | 0 src/renderer/src/{ => hooks}/mutations/projects.ts | 0 src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx | 2 +- src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename src/renderer/src/{ => hooks}/mutations/deviceInfo.ts (100%) rename src/renderer/src/{ => hooks}/mutations/projects.ts (100%) diff --git a/src/renderer/src/mutations/deviceInfo.ts b/src/renderer/src/hooks/mutations/deviceInfo.ts similarity index 100% rename from src/renderer/src/mutations/deviceInfo.ts rename to src/renderer/src/hooks/mutations/deviceInfo.ts diff --git a/src/renderer/src/mutations/projects.ts b/src/renderer/src/hooks/mutations/projects.ts similarity index 100% rename from src/renderer/src/mutations/projects.ts rename to src/renderer/src/hooks/mutations/projects.ts diff --git a/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx b/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx index d8f1470..d5d8331 100644 --- a/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx +++ b/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx @@ -20,8 +20,8 @@ import { } from '../../components/Onboarding/onboardingLogic' import { Text } from '../../components/Text' import { PROJECT_NAME_MAX_LENGTH_GRAPHEMES } from '../../constants' +import { useCreateProject } from '../../hooks/mutations/projects' import ProjectImage from '../../images/add_square.png' -import { useCreateProject } from '../../mutations/projects' export const m = defineMessages({ title: { diff --git a/src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx b/src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx index 0a524d7..bf50d78 100644 --- a/src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx +++ b/src/renderer/src/routes/Onboarding/DeviceNamingScreen.tsx @@ -14,8 +14,8 @@ import { } from '../../components/Onboarding/onboardingLogic' import { Text } from '../../components/Text' import { DEVICE_NAME_MAX_LENGTH_GRAPHEMES } from '../../constants' +import { useEditDeviceInfo } from '../../hooks/mutations/deviceInfo' import DeviceImage from '../../images/device.png' -import { useEditDeviceInfo } from '../../mutations/deviceInfo' export const m = defineMessages({ title: { From 2f2e7e45a7f0ed55faf5d2d5b894a9fba75e1bd8 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:22:31 -0800 Subject: [PATCH 08/19] chore: persisted state in context --- .../persistedState/PersistedProjectId.tsx | 45 +++++++++++++++++++ .../persistedState/createPersistedState.ts | 0 2 files changed, 45 insertions(+) create mode 100644 src/renderer/src/contexts/persistedState/PersistedProjectId.tsx rename src/renderer/src/{hooks => contexts}/persistedState/createPersistedState.ts (100%) diff --git a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx new file mode 100644 index 0000000..57911e1 --- /dev/null +++ b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx @@ -0,0 +1,45 @@ +import { createContext, useContext, useState, type ReactNode } from 'react' +import { useStore, type StateCreator } from 'zustand' + +import { createPersistedStore } from './createPersistedState' + +type ProjectIdSlice = { + projectId: string | undefined + setProjectId: (id?: string) => void +} + +const projectIdSlice: StateCreator = (set) => ({ + projectId: undefined, + setProjectId: (projectId) => set({ projectId }), +}) + +const projectIdStore = createPersistedStore(projectIdSlice, 'ActiveProjectId') + +const PersistedActiveProjectContext = createContext< + typeof projectIdStore | null +>(null) + +export const PersistedActiveProjectProvider = ({ + children, +}: { + children: ReactNode +}) => { + const [store] = useState(() => projectIdStore) + + return ( + + {children} + + ) +} + +export function usePersistedProjectIdStore( + selector: (state: ProjectIdSlice) => Selected, +): Selected { + const store = useContext(PersistedActiveProjectContext) + if (!store) { + throw new Error('Missing Persisted Project Id Store') + } + + return useStore(store, selector) +} diff --git a/src/renderer/src/hooks/persistedState/createPersistedState.ts b/src/renderer/src/contexts/persistedState/createPersistedState.ts similarity index 100% rename from src/renderer/src/hooks/persistedState/createPersistedState.ts rename to src/renderer/src/contexts/persistedState/createPersistedState.ts From f8bb4fb2c23c5710fe4be7ba2bedb687f962570b Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:23:03 -0800 Subject: [PATCH 09/19] chore: persisted project Id --- src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx | 3 +++ src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx | 7 ++++++- src/renderer/src/routes/__root.tsx | 7 +++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx b/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx index eb9a6a8..0043f62 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx +++ b/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx @@ -2,15 +2,18 @@ import * as React from 'react' import { createFileRoute } from '@tanstack/react-router' import { Text } from '../../components/Text' +import { usePersistedProjectIdStore } from '../../contexts/persistedState/PersistedProjectId' export const Route = createFileRoute('/(MapTabs)/_Map/tab1')({ component: RouteComponent, }) function RouteComponent() { + const projectId = usePersistedProjectIdStore((store) => store.projectId) return (
Tab 1 + {projectId}
) } diff --git a/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx b/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx index d5d8331..33e132e 100644 --- a/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx +++ b/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx @@ -20,6 +20,7 @@ import { } from '../../components/Onboarding/onboardingLogic' import { Text } from '../../components/Text' import { PROJECT_NAME_MAX_LENGTH_GRAPHEMES } from '../../constants' +import { usePersistedProjectIdStore } from '../../contexts/persistedState/PersistedProjectId' import { useCreateProject } from '../../hooks/mutations/projects' import ProjectImage from '../../images/add_square.png' @@ -110,6 +111,9 @@ function CreateProjectScreenComponent() { const [error, setError] = useState(false) const [errorMessage, setErrorMessage] = useState('') const setProjectNameMutation = useCreateProject() + const setPersistedProjectId = usePersistedProjectIdStore( + (store) => store.setProjectId, + ) const [configFileName, setConfigFileName] = useState(null) @@ -134,7 +138,8 @@ function CreateProjectScreenComponent() { return } setProjectNameMutation.mutate(projectName, { - onSuccess: () => { + onSuccess: (projectId) => { + setPersistedProjectId(projectId) navigate({ to: '/tab1' }) }, onError: (error) => { diff --git a/src/renderer/src/routes/__root.tsx b/src/renderer/src/routes/__root.tsx index 9946ccc..81ee29d 100644 --- a/src/renderer/src/routes/__root.tsx +++ b/src/renderer/src/routes/__root.tsx @@ -6,6 +6,7 @@ import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { theme } from '../Theme' import { ApiProvider } from '../contexts/ApiContext' import { IntlProvider } from '../contexts/IntlContext' +import { PersistedActiveProjectProvider } from '../contexts/persistedState/PersistedProjectId' const queryClient = new QueryClient() @@ -16,8 +17,10 @@ export const Route = createRootRoute({ - - + + + + From 39a8dc42ca683e7f05f902aa8515fe474368cd84 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:28:13 -0800 Subject: [PATCH 10/19] chore: create Router Context --- src/renderer/src/App.tsx | 30 ++++++++++++ src/renderer/src/AppWrapper.tsx | 28 +++++++++++ .../src/contexts/ActiveProjectIdStore.tsx | 32 +++++++++++++ .../persistedState/PersistedProjectId.tsx | 14 +++--- src/renderer/src/index.tsx | 13 +----- .../src/routes/(MapTabs)/_Map.tab1.tsx | 11 +++-- src/renderer/src/routes/(MapTabs)/_Map.tsx | 46 +++++++++++-------- src/renderer/src/routes/__root.tsx | 33 ++++--------- src/renderer/src/routes/index.tsx | 34 ++++---------- 9 files changed, 152 insertions(+), 89 deletions(-) create mode 100644 src/renderer/src/App.tsx create mode 100644 src/renderer/src/AppWrapper.tsx create mode 100644 src/renderer/src/contexts/ActiveProjectIdStore.tsx diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx new file mode 100644 index 0000000..48cc1c4 --- /dev/null +++ b/src/renderer/src/App.tsx @@ -0,0 +1,30 @@ +import { useOwnDeviceInfo } from '@comapeo/core-react' +import { RouterProvider, createRouter } from '@tanstack/react-router' + +import { usePersistedProjectIdStore } from './contexts/persistedState/PersistedProjectId' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ + routeTree, + context: { hasDeviceName: undefined!, persistedProjectId: undefined! }, +}) + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +export const App = () => { + const { data } = useOwnDeviceInfo() + const hasDeviceName = data?.name !== undefined + const persistedProjectId = usePersistedProjectIdStore( + (store) => store.projectId, + ) + return ( + + ) +} diff --git a/src/renderer/src/AppWrapper.tsx b/src/renderer/src/AppWrapper.tsx new file mode 100644 index 0000000..4d9f4ac --- /dev/null +++ b/src/renderer/src/AppWrapper.tsx @@ -0,0 +1,28 @@ +import { ThemeProvider } from '@emotion/react' +import { CssBaseline } from '@mui/material' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +import { App } from './App' +import { theme } from './Theme' +import { ApiProvider } from './contexts/ApiContext' +import { IntlProvider } from './contexts/IntlContext' +import { PersistedProjectIdProvider } from './contexts/persistedState/PersistedProjectId' + +const queryClient = new QueryClient() + +export const AppWrapper = () => { + return ( + + + + + + + + + + + + + ) +} diff --git a/src/renderer/src/contexts/ActiveProjectIdStore.tsx b/src/renderer/src/contexts/ActiveProjectIdStore.tsx new file mode 100644 index 0000000..72b063c --- /dev/null +++ b/src/renderer/src/contexts/ActiveProjectIdStore.tsx @@ -0,0 +1,32 @@ +import { createContext, useContext, type ReactNode } from 'react' + +import { usePersistedProjectIdStore } from './persistedState/PersistedProjectId' + +const ActiveProjectContext = createContext(null) + +/** + * This provider guarantees a projectId in persisted state + */ +export const ActiveProjectContextProvider = ({ + children, +}: { + children: ReactNode +}) => { + const projectId = usePersistedProjectIdStore((store) => store.projectId) + if (!projectId) { + throw new Error('No Project Id set') + } + return ( + + {children} + + ) +} + +export function useActiveProjectId() { + const context = useContext(ActiveProjectContext) + if (!context) { + throw new Error('no active project') + } + return context +} diff --git a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx index 57911e1..dd8774e 100644 --- a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx +++ b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx @@ -15,11 +15,11 @@ const projectIdSlice: StateCreator = (set) => ({ const projectIdStore = createPersistedStore(projectIdSlice, 'ActiveProjectId') -const PersistedActiveProjectContext = createContext< - typeof projectIdStore | null ->(null) +const PersistedProjectIdContext = createContext( + null, +) -export const PersistedActiveProjectProvider = ({ +export const PersistedProjectIdProvider = ({ children, }: { children: ReactNode @@ -27,16 +27,16 @@ export const PersistedActiveProjectProvider = ({ const [store] = useState(() => projectIdStore) return ( - + {children} - + ) } export function usePersistedProjectIdStore( selector: (state: ProjectIdSlice) => Selected, ): Selected { - const store = useContext(PersistedActiveProjectContext) + const store = useContext(PersistedProjectIdContext) if (!store) { throw new Error('Missing Persisted Project Id Store') } diff --git a/src/renderer/src/index.tsx b/src/renderer/src/index.tsx index 8cbe64a..1c5f3c5 100644 --- a/src/renderer/src/index.tsx +++ b/src/renderer/src/index.tsx @@ -1,18 +1,9 @@ -import { RouterProvider, createRouter } from '@tanstack/react-router' import { createRoot } from 'react-dom/client' -import { routeTree } from './routeTree.gen' - import './index.css' -const router = createRouter({ routeTree }) - -declare module '@tanstack/react-router' { - interface Register { - router: typeof router - } -} +import { AppWrapper } from './AppWrapper' const root = createRoot(document.getElementById('app') as HTMLElement) -root.render() +root.render() diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx b/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx index 0043f62..66d9755 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx +++ b/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx @@ -1,19 +1,24 @@ import * as React from 'react' +import { useManyDocs } from '@comapeo/core-react' import { createFileRoute } from '@tanstack/react-router' import { Text } from '../../components/Text' -import { usePersistedProjectIdStore } from '../../contexts/persistedState/PersistedProjectId' +import { useActiveProjectId } from '../../contexts/ActiveProjectIdStore' export const Route = createFileRoute('/(MapTabs)/_Map/tab1')({ component: RouteComponent, }) function RouteComponent() { - const projectId = usePersistedProjectIdStore((store) => store.projectId) + const projectId = useActiveProjectId() + const { data: observations } = useManyDocs({ + projectId, + docType: 'observation', + }) return (
Tab 1 - {projectId} + {`Number of observations: ${observations.length}`}
) } diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tsx b/src/renderer/src/routes/(MapTabs)/_Map.tsx index 70826f1..5a67dd5 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tsx +++ b/src/renderer/src/routes/(MapTabs)/_Map.tsx @@ -5,6 +5,7 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' import { VERY_LIGHT_GREY, WHITE } from '../../colors' import { Tabs } from '../../components/Tabs' +import { ActiveProjectContextProvider } from '../../contexts/ActiveProjectIdStore' const Container = styled('div')({ display: 'flex', @@ -14,29 +15,34 @@ const Container = styled('div')({ export const Route = createFileRoute('/(MapTabs)/_Map')({ component: MapLayout, + beforeLoad: () => { + console.log('LOADING') + }, }) export function MapLayout() { return ( - - - -
- }> - - -
-
- }> -
map component here
-
-
+ + + + +
+ }> + + +
+
+ }> +
map component here
+
+
+
) } diff --git a/src/renderer/src/routes/__root.tsx b/src/renderer/src/routes/__root.tsx index 0cea012..9dc960d 100644 --- a/src/renderer/src/routes/__root.tsx +++ b/src/renderer/src/routes/__root.tsx @@ -1,31 +1,16 @@ import { Suspense } from 'react' -import { CssBaseline, ThemeProvider } from '@mui/material' import CircularProgress from '@mui/material/CircularProgress' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { Outlet, createRootRoute } from '@tanstack/react-router' +import { Outlet, createRootRouteWithContext } from '@tanstack/react-router' -import { theme } from '../Theme' -import { ApiProvider } from '../contexts/ApiContext' -import { IntlProvider } from '../contexts/IntlContext' -import { PersistedActiveProjectProvider } from '../contexts/persistedState/PersistedProjectId' +type RouterContext = { + hasDeviceName: boolean + persistedProjectId: string | undefined +} -const queryClient = new QueryClient() - -export const Route = createRootRoute({ +export const Route = createRootRouteWithContext()({ component: () => ( - - - - - - - }> - - - - - - - + }> + + ), }) diff --git a/src/renderer/src/routes/index.tsx b/src/renderer/src/routes/index.tsx index 5254a96..5a65dd9 100644 --- a/src/renderer/src/routes/index.tsx +++ b/src/renderer/src/routes/index.tsx @@ -1,27 +1,13 @@ -import { useLayoutEffect } from 'react' -import { createFileRoute, useNavigate } from '@tanstack/react-router' - -import { useDeviceInfo } from '../queries/deviceInfo' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/')({ - component: RouteComponent, -}) - -// the user will never get here, they will be redirected. -// While this code is semi hacky, the suggested alternative is to redirect in the (beforeLoad)[https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#the-routebeforeload-option]. The problem is that this requires the router to use 'useDeviceInfo'. We could pass this hook to the router via the RouterContext. But i think the complexity of passing that info makes this hacky code slightly more desirable and easy to understand. - -function RouteComponent() { - const navigate = useNavigate() - const { data } = useDeviceInfo() - const hasCreatedDeviceName = data?.name !== undefined - - useLayoutEffect(() => { - if (!hasCreatedDeviceName) { - navigate({ to: '/Onboarding' }) - } else { - navigate({ to: '/tab1' }) + beforeLoad: ({ context }) => { + if (!context.hasDeviceName) { + throw redirect({ to: '/Onboarding' }) } - }) - - return null -} + if (!context.persistedProjectId) { + throw redirect({ to: '/Onboarding/CreateJoinProjectScreen' }) + } + throw redirect({ to: '/tab1' }) + }, +}) From a09fa30b3d9312a9caae5fe2a9562e7143c64744 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:33:10 -0800 Subject: [PATCH 11/19] chore:simplify folder structure --- src/renderer/src/routeTree.gen.ts | 218 +++++++----------- .../routes/Onboarding/CreateProjectScreen.tsx | 2 +- src/renderer/src/routes/Welcome.tsx | 17 -- .../src/routes/{(MapTabs) => }/_Map.test.tsx | 2 +- .../src/routes/{(MapTabs) => }/_Map.tsx | 8 +- .../_Map.tab1.tsx => _Map/Tab1.tsx} | 2 +- .../_Map.tab2.tsx => _Map/Tab2.tsx} | 2 +- src/renderer/src/routes/index.tsx | 2 +- 8 files changed, 87 insertions(+), 166 deletions(-) delete mode 100644 src/renderer/src/routes/Welcome.tsx rename src/renderer/src/routes/{(MapTabs) => }/_Map.test.tsx (96%) rename src/renderer/src/routes/{(MapTabs) => }/_Map.tsx (79%) rename src/renderer/src/routes/{(MapTabs)/_Map.tab1.tsx => _Map/Tab1.tsx} (90%) rename src/renderer/src/routes/{(MapTabs)/_Map.tab2.tsx => _Map/Tab2.tsx} (79%) diff --git a/src/renderer/src/routeTree.gen.ts b/src/renderer/src/routeTree.gen.ts index 1f8e799..30c0192 100644 --- a/src/renderer/src/routeTree.gen.ts +++ b/src/renderer/src/routeTree.gen.ts @@ -8,38 +8,25 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { createFileRoute } from '@tanstack/react-router' - // Import Routes import { Route as rootRoute } from './routes/__root' -import { Route as WelcomeImport } from './routes/Welcome' +import { Route as MapImport } from './routes/_Map' import { Route as IndexImport } from './routes/index' import { Route as OnboardingIndexImport } from './routes/Onboarding/index' +import { Route as MapTab2Import } from './routes/_Map/Tab2' +import { Route as MapTab1Import } from './routes/_Map/Tab1' import { Route as OnboardingPrivacyPolicyScreenImport } from './routes/Onboarding/PrivacyPolicyScreen' import { Route as OnboardingJoinProjectScreenImport } from './routes/Onboarding/JoinProjectScreen' import { Route as OnboardingDeviceNamingScreenImport } from './routes/Onboarding/DeviceNamingScreen' import { Route as OnboardingDataPrivacyImport } from './routes/Onboarding/DataPrivacy' import { Route as OnboardingCreateProjectScreenImport } from './routes/Onboarding/CreateProjectScreen' import { Route as OnboardingCreateJoinProjectScreenImport } from './routes/Onboarding/CreateJoinProjectScreen' -import { Route as MapTabsMapImport } from './routes/(MapTabs)/_Map' -import { Route as MapTabsMapTab2Import } from './routes/(MapTabs)/_Map.tab2' -import { Route as MapTabsMapTab1Import } from './routes/(MapTabs)/_Map.tab1' - -// Create Virtual Routes - -const MapTabsImport = createFileRoute('/(MapTabs)')() // Create/Update Routes -const MapTabsRoute = MapTabsImport.update({ - id: '/(MapTabs)', - getParentRoute: () => rootRoute, -} as any) - -const WelcomeRoute = WelcomeImport.update({ - id: '/Welcome', - path: '/Welcome', +const MapRoute = MapImport.update({ + id: '/_Map', getParentRoute: () => rootRoute, } as any) @@ -55,6 +42,18 @@ const OnboardingIndexRoute = OnboardingIndexImport.update({ getParentRoute: () => rootRoute, } as any) +const MapTab2Route = MapTab2Import.update({ + id: '/Tab2', + path: '/Tab2', + getParentRoute: () => MapRoute, +} as any) + +const MapTab1Route = MapTab1Import.update({ + id: '/Tab1', + path: '/Tab1', + getParentRoute: () => MapRoute, +} as any) + const OnboardingPrivacyPolicyScreenRoute = OnboardingPrivacyPolicyScreenImport.update({ id: '/Onboarding/PrivacyPolicyScreen', @@ -96,23 +95,6 @@ const OnboardingCreateJoinProjectScreenRoute = getParentRoute: () => rootRoute, } as any) -const MapTabsMapRoute = MapTabsMapImport.update({ - id: '/_Map', - getParentRoute: () => MapTabsRoute, -} as any) - -const MapTabsMapTab2Route = MapTabsMapTab2Import.update({ - id: '/tab2', - path: '/tab2', - getParentRoute: () => MapTabsMapRoute, -} as any) - -const MapTabsMapTab1Route = MapTabsMapTab1Import.update({ - id: '/tab1', - path: '/tab1', - getParentRoute: () => MapTabsMapRoute, -} as any) - // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -124,27 +106,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } - '/Welcome': { - id: '/Welcome' - path: '/Welcome' - fullPath: '/Welcome' - preLoaderRoute: typeof WelcomeImport - parentRoute: typeof rootRoute - } - '/(MapTabs)': { - id: '/(MapTabs)' - path: '/' - fullPath: '/' - preLoaderRoute: typeof MapTabsImport + '/_Map': { + id: '/_Map' + path: '' + fullPath: '' + preLoaderRoute: typeof MapImport parentRoute: typeof rootRoute } - '/(MapTabs)/_Map': { - id: '/(MapTabs)/_Map' - path: '/' - fullPath: '/' - preLoaderRoute: typeof MapTabsMapImport - parentRoute: typeof MapTabsRoute - } '/Onboarding/CreateJoinProjectScreen': { id: '/Onboarding/CreateJoinProjectScreen' path: '/Onboarding/CreateJoinProjectScreen' @@ -187,6 +155,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OnboardingPrivacyPolicyScreenImport parentRoute: typeof rootRoute } + '/_Map/Tab1': { + id: '/_Map/Tab1' + path: '/Tab1' + fullPath: '/Tab1' + preLoaderRoute: typeof MapTab1Import + parentRoute: typeof MapImport + } + '/_Map/Tab2': { + id: '/_Map/Tab2' + path: '/Tab2' + fullPath: '/Tab2' + preLoaderRoute: typeof MapTab2Import + parentRoute: typeof MapImport + } '/Onboarding/': { id: '/Onboarding/' path: '/Onboarding' @@ -194,144 +176,112 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OnboardingIndexImport parentRoute: typeof rootRoute } - '/(MapTabs)/_Map/tab1': { - id: '/(MapTabs)/_Map/tab1' - path: '/tab1' - fullPath: '/tab1' - preLoaderRoute: typeof MapTabsMapTab1Import - parentRoute: typeof MapTabsMapImport - } - '/(MapTabs)/_Map/tab2': { - id: '/(MapTabs)/_Map/tab2' - path: '/tab2' - fullPath: '/tab2' - preLoaderRoute: typeof MapTabsMapTab2Import - parentRoute: typeof MapTabsMapImport - } } } // Create and export the route tree -interface MapTabsMapRouteChildren { - MapTabsMapTab1Route: typeof MapTabsMapTab1Route - MapTabsMapTab2Route: typeof MapTabsMapTab2Route -} - -const MapTabsMapRouteChildren: MapTabsMapRouteChildren = { - MapTabsMapTab1Route: MapTabsMapTab1Route, - MapTabsMapTab2Route: MapTabsMapTab2Route, -} - -const MapTabsMapRouteWithChildren = MapTabsMapRoute._addFileChildren( - MapTabsMapRouteChildren, -) - -interface MapTabsRouteChildren { - MapTabsMapRoute: typeof MapTabsMapRouteWithChildren +interface MapRouteChildren { + MapTab1Route: typeof MapTab1Route + MapTab2Route: typeof MapTab2Route } -const MapTabsRouteChildren: MapTabsRouteChildren = { - MapTabsMapRoute: MapTabsMapRouteWithChildren, +const MapRouteChildren: MapRouteChildren = { + MapTab1Route: MapTab1Route, + MapTab2Route: MapTab2Route, } -const MapTabsRouteWithChildren = - MapTabsRoute._addFileChildren(MapTabsRouteChildren) +const MapRouteWithChildren = MapRoute._addFileChildren(MapRouteChildren) export interface FileRoutesByFullPath { - '/': typeof MapTabsMapRouteWithChildren - '/Welcome': typeof WelcomeRoute + '/': typeof IndexRoute + '': typeof MapRouteWithChildren '/Onboarding/CreateJoinProjectScreen': typeof OnboardingCreateJoinProjectScreenRoute '/Onboarding/CreateProjectScreen': typeof OnboardingCreateProjectScreenRoute '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute '/Onboarding/DeviceNamingScreen': typeof OnboardingDeviceNamingScreenRoute '/Onboarding/JoinProjectScreen': typeof OnboardingJoinProjectScreenRoute '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute + '/Tab1': typeof MapTab1Route + '/Tab2': typeof MapTab2Route '/Onboarding': typeof OnboardingIndexRoute - '/tab1': typeof MapTabsMapTab1Route - '/tab2': typeof MapTabsMapTab2Route } export interface FileRoutesByTo { - '/': typeof MapTabsMapRouteWithChildren - '/Welcome': typeof WelcomeRoute + '/': typeof IndexRoute + '': typeof MapRouteWithChildren '/Onboarding/CreateJoinProjectScreen': typeof OnboardingCreateJoinProjectScreenRoute '/Onboarding/CreateProjectScreen': typeof OnboardingCreateProjectScreenRoute '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute '/Onboarding/DeviceNamingScreen': typeof OnboardingDeviceNamingScreenRoute '/Onboarding/JoinProjectScreen': typeof OnboardingJoinProjectScreenRoute '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute + '/Tab1': typeof MapTab1Route + '/Tab2': typeof MapTab2Route '/Onboarding': typeof OnboardingIndexRoute - '/tab1': typeof MapTabsMapTab1Route - '/tab2': typeof MapTabsMapTab2Route } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute - '/Welcome': typeof WelcomeRoute - '/(MapTabs)': typeof MapTabsRouteWithChildren - '/(MapTabs)/_Map': typeof MapTabsMapRouteWithChildren + '/_Map': typeof MapRouteWithChildren '/Onboarding/CreateJoinProjectScreen': typeof OnboardingCreateJoinProjectScreenRoute '/Onboarding/CreateProjectScreen': typeof OnboardingCreateProjectScreenRoute '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute '/Onboarding/DeviceNamingScreen': typeof OnboardingDeviceNamingScreenRoute '/Onboarding/JoinProjectScreen': typeof OnboardingJoinProjectScreenRoute '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute + '/_Map/Tab1': typeof MapTab1Route + '/_Map/Tab2': typeof MapTab2Route '/Onboarding/': typeof OnboardingIndexRoute - '/(MapTabs)/_Map/tab1': typeof MapTabsMapTab1Route - '/(MapTabs)/_Map/tab2': typeof MapTabsMapTab2Route } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' - | '/Welcome' + | '' | '/Onboarding/CreateJoinProjectScreen' | '/Onboarding/CreateProjectScreen' | '/Onboarding/DataPrivacy' | '/Onboarding/DeviceNamingScreen' | '/Onboarding/JoinProjectScreen' | '/Onboarding/PrivacyPolicyScreen' + | '/Tab1' + | '/Tab2' | '/Onboarding' - | '/tab1' - | '/tab2' fileRoutesByTo: FileRoutesByTo to: | '/' - | '/Welcome' + | '' | '/Onboarding/CreateJoinProjectScreen' | '/Onboarding/CreateProjectScreen' | '/Onboarding/DataPrivacy' | '/Onboarding/DeviceNamingScreen' | '/Onboarding/JoinProjectScreen' | '/Onboarding/PrivacyPolicyScreen' + | '/Tab1' + | '/Tab2' | '/Onboarding' - | '/tab1' - | '/tab2' id: | '__root__' | '/' - | '/Welcome' - | '/(MapTabs)' - | '/(MapTabs)/_Map' + | '/_Map' | '/Onboarding/CreateJoinProjectScreen' | '/Onboarding/CreateProjectScreen' | '/Onboarding/DataPrivacy' | '/Onboarding/DeviceNamingScreen' | '/Onboarding/JoinProjectScreen' | '/Onboarding/PrivacyPolicyScreen' + | '/_Map/Tab1' + | '/_Map/Tab2' | '/Onboarding/' - | '/(MapTabs)/_Map/tab1' - | '/(MapTabs)/_Map/tab2' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - WelcomeRoute: typeof WelcomeRoute - MapTabsRoute: typeof MapTabsRouteWithChildren + MapRoute: typeof MapRouteWithChildren OnboardingCreateJoinProjectScreenRoute: typeof OnboardingCreateJoinProjectScreenRoute OnboardingCreateProjectScreenRoute: typeof OnboardingCreateProjectScreenRoute OnboardingDataPrivacyRoute: typeof OnboardingDataPrivacyRoute @@ -343,8 +293,7 @@ export interface RootRouteChildren { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - WelcomeRoute: WelcomeRoute, - MapTabsRoute: MapTabsRouteWithChildren, + MapRoute: MapRouteWithChildren, OnboardingCreateJoinProjectScreenRoute: OnboardingCreateJoinProjectScreenRoute, OnboardingCreateProjectScreenRoute: OnboardingCreateProjectScreenRoute, @@ -366,8 +315,7 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", - "/Welcome", - "/(MapTabs)", + "/_Map", "/Onboarding/CreateJoinProjectScreen", "/Onboarding/CreateProjectScreen", "/Onboarding/DataPrivacy", @@ -380,21 +328,11 @@ export const routeTree = rootRoute "/": { "filePath": "index.tsx" }, - "/Welcome": { - "filePath": "Welcome.tsx" - }, - "/(MapTabs)": { - "filePath": "(MapTabs)", + "/_Map": { + "filePath": "_Map.tsx", "children": [ - "/(MapTabs)/_Map" - ] - }, - "/(MapTabs)/_Map": { - "filePath": "(MapTabs)/_Map.tsx", - "parent": "/(MapTabs)", - "children": [ - "/(MapTabs)/_Map/tab1", - "/(MapTabs)/_Map/tab2" + "/_Map/Tab1", + "/_Map/Tab2" ] }, "/Onboarding/CreateJoinProjectScreen": { @@ -415,16 +353,16 @@ export const routeTree = rootRoute "/Onboarding/PrivacyPolicyScreen": { "filePath": "Onboarding/PrivacyPolicyScreen.tsx" }, - "/Onboarding/": { - "filePath": "Onboarding/index.tsx" + "/_Map/Tab1": { + "filePath": "_Map/Tab1.tsx", + "parent": "/_Map" }, - "/(MapTabs)/_Map/tab1": { - "filePath": "(MapTabs)/_Map.tab1.tsx", - "parent": "/(MapTabs)/_Map" + "/_Map/Tab2": { + "filePath": "_Map/Tab2.tsx", + "parent": "/_Map" }, - "/(MapTabs)/_Map/tab2": { - "filePath": "(MapTabs)/_Map.tab2.tsx", - "parent": "/(MapTabs)/_Map" + "/Onboarding/": { + "filePath": "Onboarding/index.tsx" } } } diff --git a/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx b/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx index 33e132e..2779535 100644 --- a/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx +++ b/src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx @@ -140,7 +140,7 @@ function CreateProjectScreenComponent() { setProjectNameMutation.mutate(projectName, { onSuccess: (projectId) => { setPersistedProjectId(projectId) - navigate({ to: '/tab1' }) + navigate({ to: '/Tab1' }) }, onError: (error) => { console.error('Error setting project name:', error) diff --git a/src/renderer/src/routes/Welcome.tsx b/src/renderer/src/routes/Welcome.tsx deleted file mode 100644 index 3a9129e..0000000 --- a/src/renderer/src/routes/Welcome.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react' -import { Link, createFileRoute } from '@tanstack/react-router' - -import { Text } from '../components/Text' - -export const Route = createFileRoute('/Welcome')({ - component: RouteComponent, -}) - -function RouteComponent() { - return ( -
- Welcome Page - Map -
- ) -} diff --git a/src/renderer/src/routes/(MapTabs)/_Map.test.tsx b/src/renderer/src/routes/_Map.test.tsx similarity index 96% rename from src/renderer/src/routes/(MapTabs)/_Map.test.tsx rename to src/renderer/src/routes/_Map.test.tsx index 34a7182..2f601c3 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.test.tsx +++ b/src/renderer/src/routes/_Map.test.tsx @@ -8,7 +8,7 @@ import { import { render, screen } from '@testing-library/react' import { expect, test } from 'vitest' -import { IntlProvider } from '../../contexts/IntlContext' +import { IntlProvider } from '../contexts/IntlContext' import { MapLayout } from './_Map' const rootRoute = createRootRoute({}) diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tsx b/src/renderer/src/routes/_Map.tsx similarity index 79% rename from src/renderer/src/routes/(MapTabs)/_Map.tsx rename to src/renderer/src/routes/_Map.tsx index 5a67dd5..1d5cea9 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tsx +++ b/src/renderer/src/routes/_Map.tsx @@ -3,9 +3,9 @@ import { CircularProgress, Paper } from '@mui/material' import { styled } from '@mui/material/styles' import { Outlet, createFileRoute } from '@tanstack/react-router' -import { VERY_LIGHT_GREY, WHITE } from '../../colors' -import { Tabs } from '../../components/Tabs' -import { ActiveProjectContextProvider } from '../../contexts/ActiveProjectIdStore' +import { VERY_LIGHT_GREY, WHITE } from '../colors' +import { Tabs } from '../components/Tabs' +import { ActiveProjectContextProvider } from '../contexts/ActiveProjectIdStore' const Container = styled('div')({ display: 'flex', @@ -13,7 +13,7 @@ const Container = styled('div')({ height: '100%', }) -export const Route = createFileRoute('/(MapTabs)/_Map')({ +export const Route = createFileRoute('/_Map')({ component: MapLayout, beforeLoad: () => { console.log('LOADING') diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx b/src/renderer/src/routes/_Map/Tab1.tsx similarity index 90% rename from src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx rename to src/renderer/src/routes/_Map/Tab1.tsx index 66d9755..efac5ac 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx +++ b/src/renderer/src/routes/_Map/Tab1.tsx @@ -5,7 +5,7 @@ import { createFileRoute } from '@tanstack/react-router' import { Text } from '../../components/Text' import { useActiveProjectId } from '../../contexts/ActiveProjectIdStore' -export const Route = createFileRoute('/(MapTabs)/_Map/tab1')({ +export const Route = createFileRoute('/_Map/Tab1')({ component: RouteComponent, }) diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tab2.tsx b/src/renderer/src/routes/_Map/Tab2.tsx similarity index 79% rename from src/renderer/src/routes/(MapTabs)/_Map.tab2.tsx rename to src/renderer/src/routes/_Map/Tab2.tsx index 5171d08..b8aff78 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tab2.tsx +++ b/src/renderer/src/routes/_Map/Tab2.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from '@tanstack/react-router' import { Text } from '../../components/Text' -export const Route = createFileRoute('/(MapTabs)/_Map/tab2')({ +export const Route = createFileRoute('/_Map/Tab2')({ component: Settings, }) diff --git a/src/renderer/src/routes/index.tsx b/src/renderer/src/routes/index.tsx index 5a65dd9..abad31a 100644 --- a/src/renderer/src/routes/index.tsx +++ b/src/renderer/src/routes/index.tsx @@ -8,6 +8,6 @@ export const Route = createFileRoute('/')({ if (!context.persistedProjectId) { throw redirect({ to: '/Onboarding/CreateJoinProjectScreen' }) } - throw redirect({ to: '/tab1' }) + throw redirect({ to: '/Tab1' }) }, }) From 9423992d3b61cc0640470bf1c0ecc1cc39965ae9 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:34:46 -0800 Subject: [PATCH 12/19] chore: tabs --- src/renderer/src/components/Tabs.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/Tabs.tsx b/src/renderer/src/components/Tabs.tsx index 3d3be44..94f1323 100644 --- a/src/renderer/src/components/Tabs.tsx +++ b/src/renderer/src/components/Tabs.tsx @@ -47,7 +47,7 @@ export const Tabs = () => { } - value={'/tab1'} + value={'/Tab1'} /> {/* This is needed to properly space the items. Originally used a div, but was causing console errors as the Parent component passes it props, which were invalid for non-tab components */} @@ -59,7 +59,7 @@ export const Tabs = () => { {formatMessage(m.setting)} } - value={'/tab2'} + value={'/Tab2'} /> } @@ -68,7 +68,7 @@ export const Tabs = () => { {formatMessage(m.about)} } - value={'/tab2'} + value={'/Tab2'} /> ) @@ -77,7 +77,7 @@ export const Tabs = () => { type TabProps = React.ComponentProps type MapTabRoute = { - [K in keyof FileRoutesById]: K extends `${'/(MapTabs)/_Map'}${infer Rest}` + [K in keyof FileRoutesById]: K extends `${'/_Map'}${infer Rest}` ? Rest extends '' ? never : `${Rest}` From 55a8b21aa06ebe172010c53c0742b46e93884ee3 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:04:21 -0800 Subject: [PATCH 13/19] chore: create reusable persisted state function --- src/renderer/src/App.tsx | 2 +- .../persistedState/PersistedProjectId.tsx | 64 +++++++++---------- .../persistedState/createPersistedState.ts | 25 -------- .../persistedState/createPersistedState.tsx | 55 ++++++++++++++++ src/renderer/src/routes/_Map.tsx | 3 - 5 files changed, 87 insertions(+), 62 deletions(-) delete mode 100644 src/renderer/src/contexts/persistedState/createPersistedState.ts create mode 100644 src/renderer/src/contexts/persistedState/createPersistedState.tsx diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 48cc1c4..30a2344 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -18,7 +18,7 @@ declare module '@tanstack/react-router' { export const App = () => { const { data } = useOwnDeviceInfo() const hasDeviceName = data?.name !== undefined - const persistedProjectId = usePersistedProjectIdStore( + const persistedProjectId = !!usePersistedProjectIdStore( (store) => store.projectId, ) return ( diff --git a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx index dd8774e..1c39a13 100644 --- a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx +++ b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx @@ -1,7 +1,6 @@ -import { createContext, useContext, useState, type ReactNode } from 'react' -import { useStore, type StateCreator } from 'zustand' +import { type StateCreator } from 'zustand' -import { createPersistedStore } from './createPersistedState' +import { createPersistedStoreWithProvider } from './createPersistedState' type ProjectIdSlice = { projectId: string | undefined @@ -13,33 +12,32 @@ const projectIdSlice: StateCreator = (set) => ({ setProjectId: (projectId) => set({ projectId }), }) -const projectIdStore = createPersistedStore(projectIdSlice, 'ActiveProjectId') - -const PersistedProjectIdContext = createContext( - null, -) - -export const PersistedProjectIdProvider = ({ - children, -}: { - children: ReactNode -}) => { - const [store] = useState(() => projectIdStore) - - return ( - - {children} - - ) -} - -export function usePersistedProjectIdStore( - selector: (state: ProjectIdSlice) => Selected, -): Selected { - const store = useContext(PersistedProjectIdContext) - if (!store) { - throw new Error('Missing Persisted Project Id Store') - } - - return useStore(store, selector) -} +export const { + Provider: PersistedProjectIdProvider, + useStoreHook: usePersistedProjectIdStore, +} = createPersistedStoreWithProvider(projectIdSlice, 'ActiveProjectId') + +// export const PersistedProjectIdProvider = ({ +// children, +// }: { +// children: ReactNode +// }) => { +// const [store] = useState(() => projectIdStore) + +// return ( +// +// {children} +// +// ) +// } + +// export function usePersistedProjectIdStore( +// selector: (state: ProjectIdSlice) => Selected, +// ): Selected { +// const store = useContext(PersistedProjectIdContext) +// if (!store) { +// throw new Error('Missing Persisted Project Id Store') +// } + +// return useStore(store, selector) +// } diff --git a/src/renderer/src/contexts/persistedState/createPersistedState.ts b/src/renderer/src/contexts/persistedState/createPersistedState.ts deleted file mode 100644 index 2054b43..0000000 --- a/src/renderer/src/contexts/persistedState/createPersistedState.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createStore, type StateCreator } from 'zustand' -import { persist } from 'zustand/middleware' - -type PersistedStoreKey = 'ActiveProjectId' - -export function createPersistedStore( - ...args: Parameters> -) { - const store = createStore()(createPersistMiddleware(...args)) - store.setState((state) => ({ - ...state, - ...args[0], - })) - - return store -} - -function createPersistMiddleware( - slice: StateCreator, - persistedStoreKey: PersistedStoreKey, -) { - return persist(slice, { - name: persistedStoreKey, - }) -} diff --git a/src/renderer/src/contexts/persistedState/createPersistedState.tsx b/src/renderer/src/contexts/persistedState/createPersistedState.tsx new file mode 100644 index 0000000..2c32b0d --- /dev/null +++ b/src/renderer/src/contexts/persistedState/createPersistedState.tsx @@ -0,0 +1,55 @@ +import { createContext, useContext, useState, type ReactNode } from 'react' +import { createStore, useStore, type StateCreator } from 'zustand' +import { persist } from 'zustand/middleware' + +type PersistedStoreKey = 'ActiveProjectId' + +export function createPersistedStoreWithProvider( + slice: StateCreator, + persistedStoreKey: PersistedStoreKey, +) { + const store = createPersistedStore(slice, persistedStoreKey) + const Context = createContext(null) + + const Provider = ({ children }: { children: ReactNode }) => { + const [storeInstance] = useState(() => store) + + return {children} + } + + const useStoreHook = ( + selector: (state: T) => Selected, + ): Selected => { + const contextStore = useContext(Context) + if (!contextStore) { + throw new Error( + `Missing provider for persisted store: ${persistedStoreKey}`, + ) + } + + return useStore(contextStore, selector) + } + + return { Provider, useStoreHook } +} + +function createPersistedStore( + ...args: Parameters> +) { + const store = createStore()(createPersistMiddleware(...args)) + store.setState((state) => ({ + ...state, + ...args[0], + })) + + return store +} + +function createPersistMiddleware( + slice: StateCreator, + persistedStoreKey: PersistedStoreKey, +) { + return persist(slice, { + name: persistedStoreKey, + }) +} diff --git a/src/renderer/src/routes/_Map.tsx b/src/renderer/src/routes/_Map.tsx index 1d5cea9..763c3c1 100644 --- a/src/renderer/src/routes/_Map.tsx +++ b/src/renderer/src/routes/_Map.tsx @@ -15,9 +15,6 @@ const Container = styled('div')({ export const Route = createFileRoute('/_Map')({ component: MapLayout, - beforeLoad: () => { - console.log('LOADING') - }, }) export function MapLayout() { From 6eae33e6a09839698c1c0dd0d84a67e1b0c3f0c5 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:05:59 -0800 Subject: [PATCH 14/19] chore: make context agnostic --- src/renderer/src/routes/__root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/routes/__root.tsx b/src/renderer/src/routes/__root.tsx index 9dc960d..ac8284f 100644 --- a/src/renderer/src/routes/__root.tsx +++ b/src/renderer/src/routes/__root.tsx @@ -4,7 +4,7 @@ import { Outlet, createRootRouteWithContext } from '@tanstack/react-router' type RouterContext = { hasDeviceName: boolean - persistedProjectId: string | undefined + persistedProjectId: boolean } export const Route = createRootRouteWithContext()({ From 93cb304a7726d071b208f389c562ee7ad526dc15 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:07:41 -0800 Subject: [PATCH 15/19] chore: remove commented out code --- .../persistedState/PersistedProjectId.tsx | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx index 1c39a13..5303b92 100644 --- a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx +++ b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx @@ -16,28 +16,3 @@ export const { Provider: PersistedProjectIdProvider, useStoreHook: usePersistedProjectIdStore, } = createPersistedStoreWithProvider(projectIdSlice, 'ActiveProjectId') - -// export const PersistedProjectIdProvider = ({ -// children, -// }: { -// children: ReactNode -// }) => { -// const [store] = useState(() => projectIdStore) - -// return ( -// -// {children} -// -// ) -// } - -// export function usePersistedProjectIdStore( -// selector: (state: ProjectIdSlice) => Selected, -// ): Selected { -// const store = useContext(PersistedProjectIdContext) -// if (!store) { -// throw new Error('Missing Persisted Project Id Store') -// } - -// return useStore(store, selector) -// } From 4dcbb0fe4cb724d1a4d7880e65199dabbc93b415 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:04:22 -0800 Subject: [PATCH 16/19] chore: update test for layout to be meaningful --- package-lock.json | 109 +++++++++++++--- package.json | 1 + .../Onboarding/DeviceNamingScreen.test.tsx | 4 +- src/renderer/src/routes/_Map.test.tsx | 122 ++++++++++++------ src/renderer/src/test/setup.ts | 1 + src/renderer/vite.config.js | 1 + 6 files changed, 184 insertions(+), 54 deletions(-) create mode 100644 src/renderer/src/test/setup.ts diff --git a/package-lock.json b/package-lock.json index 371e851..51db48d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,8 +51,8 @@ "@tanstack/router-devtools": "^1.82.1", "@tanstack/router-plugin": "^1.81.9", "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.1.0", - "@testing-library/user-event": "14.5.2", "@types/eslint__js": "^8.42.3", "@types/lint-staged": "^13.3.0", "@types/node": "^20.17.6", @@ -81,6 +81,13 @@ "zustand": "5.0.2" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", + "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -4323,6 +4330,48 @@ "node": ">=18" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/react": { "version": "16.1.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.1.0.tgz", @@ -4351,20 +4400,6 @@ } } }, - "node_modules/@testing-library/user-event": { - "version": "14.5.2", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", - "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -7171,6 +7206,13 @@ "node": ">=12.10" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssstyle": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", @@ -12506,6 +12548,16 @@ "node": ">=4" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -14736,6 +14788,20 @@ "node": ">= 10.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -15974,6 +16040,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index 89cd396..e6ce2eb 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@tanstack/router-devtools": "^1.82.1", "@tanstack/router-plugin": "^1.81.9", "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.1.0", "@types/eslint__js": "^8.42.3", "@types/lint-staged": "^13.3.0", diff --git a/src/renderer/src/routes/Onboarding/DeviceNamingScreen.test.tsx b/src/renderer/src/routes/Onboarding/DeviceNamingScreen.test.tsx index 131a8bc..4f28ce0 100644 --- a/src/renderer/src/routes/Onboarding/DeviceNamingScreen.test.tsx +++ b/src/renderer/src/routes/Onboarding/DeviceNamingScreen.test.tsx @@ -1,7 +1,9 @@ -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' import { getUtf8ByteLength } from './DeviceNamingScreen' +vi.mock('@comapeo/core-react', () => ({})) + test('should return the correct byte length for ASCII characters', () => { const text = 'hello' const result = getUtf8ByteLength(text) diff --git a/src/renderer/src/routes/_Map.test.tsx b/src/renderer/src/routes/_Map.test.tsx index 2f601c3..b490ea1 100644 --- a/src/renderer/src/routes/_Map.test.tsx +++ b/src/renderer/src/routes/_Map.test.tsx @@ -1,54 +1,100 @@ import type { ReactNode } from 'react' -import { - RouterProvider, - createRootRoute, - createRoute, - createRouter, -} from '@tanstack/react-router' +import { RouterProvider, createRouter } from '@tanstack/react-router' import { render, screen } from '@testing-library/react' -import { expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' import { IntlProvider } from '../contexts/IntlContext' -import { MapLayout } from './_Map' +import { routeTree } from '../routeTree.gen' -const rootRoute = createRootRoute({}) +vi.mock('../contexts/persistedState/PersistedProjectId', () => ({ + ...vi.importActual('../contexts/persistedState/PersistedProjectId'), + usePersistedProjectIdStore: vi.fn((selector) => { + // Provide the mocked store state here + const mockedState = { + projectId: 'mocked-project-id', + setProjectId: vi.fn(), + } + return selector(mockedState) + }), +})) + +vi.mock('@comapeo/core-react', () => ({ + useManyDocs: vi.fn(() => ({ data: [] })), +})) const Wrapper = ({ children }: { children: ReactNode }) => ( {children} ) -// Creates a stubbed out router. We are just testing whether the navigation gets passed the correct route (aka "/tab1" or "/tab2") so we do not need the actual router and can just intecept the navgiation state. -const mapRoute = createRoute({ - getParentRoute: () => rootRoute, - id: 'map', - component: MapLayout, +const router = createRouter({ + routeTree, + context: { hasDeviceName: true, persistedProjectId: true }, }) -const catchAllRoute = createRoute({ - getParentRoute: () => mapRoute, - path: '$', - component: () => null, -}) +describe('clicking tabs navigate to correct tab', () => { + router.navigate({ to: '/Tab1' }) + render(, { wrapper: Wrapper }) -const routeTree = rootRoute.addChildren([mapRoute.addChildren([catchAllRoute])]) + test('There are 4 tabs', async () => { + const AllTabs = await screen.findAllByRole('tab') + expect(AllTabs).toHaveLength(4) + }) -const router = createRouter({ routeTree }) + test('Second tab has no children and is disabled', async () => { + const AllTabs = await screen.findAllByRole('tab') + const secondTab = AllTabs[1] + expect(secondTab?.childElementCount).toBe(0) + expect(secondTab).toBeDisabled() + }) -test('clicking tabs navigate to correct tab', () => { - // @ts-expect-error - typings - render(, { wrapper: Wrapper }) - const settingsButton = screen.getByText('Settings') - settingsButton.click() - const settingsRouteName = router.state.location.pathname - expect(settingsRouteName).toStrictEqual('/tab2') - - const observationTab = screen.getByTestId('tab-observation') - observationTab.click() - const observationTabRouteName = router.state.location.pathname - expect(observationTabRouteName).toStrictEqual('/tab1') - - const aboutTab = screen.getByText('About') - aboutTab.click() - const aboutTabRoute = router.state.location.pathname - expect(aboutTabRoute).toStrictEqual('/tab2') + test('Tab 1 is selected by default', async () => { + const Tab1 = screen.getByTestId('tab-observation') + expect(Tab1.ariaSelected).toBe('true') + }) + + test('The other tabs are not selected by default', () => { + const AllTabs = screen.getAllByRole('tab') + const selectedTabs = AllTabs.filter( + (tab) => tab.getAttribute('aria-selected') === 'true', + ) + expect(selectedTabs).toHaveLength(1) + }) + + test('Tab 1 Screen is showing', async () => { + const title = await screen.findByText('Tab 1') + expect(title).toBeVisible() + }) + + test('Clicking "Settings" propogates "/Tab 2" to the navigator', () => { + const settingsButton = screen.getByText('Settings') + settingsButton.click() + expect(router.state.location.pathname).toStrictEqual('/Tab2') + }) + + test('Tab 2 Screen is showing', () => { + const title = screen.getByText('Tab 2') + expect(title).toBeVisible() + }) + + test('Clicking Top Tab propogated "/Tab1" to the navigator', () => { + const firstTab = screen.getByTestId('tab-observation') + firstTab.click() + expect(router.state.location.pathname).toStrictEqual('/Tab1') + }) + + test('Tab 1 Screen is showing', async () => { + const title = await screen.findByText('Tab 1') + expect(title).toBeVisible() + }) + + test('Clicking "About" propogates "/Tab 2" to the navigator', () => { + const aboutTab = screen.getByText('About') + aboutTab.click() + expect(router.state.location.pathname).toStrictEqual('/Tab2') + }) + + test('Tab 2 Screen is showing', () => { + const title = screen.getByText('Tab 2') + expect(title).toBeDefined() + }) }) diff --git a/src/renderer/src/test/setup.ts b/src/renderer/src/test/setup.ts new file mode 100644 index 0000000..a9d0dd3 --- /dev/null +++ b/src/renderer/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/src/renderer/vite.config.js b/src/renderer/vite.config.js index 0c6514f..9d618b0 100644 --- a/src/renderer/vite.config.js +++ b/src/renderer/vite.config.js @@ -43,6 +43,7 @@ export default defineConfig((configEnv) => { ], test: { environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], }, } }) From 82df11950fb3ee30e08054a0710cd4061d209056 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:00:19 -0800 Subject: [PATCH 17/19] chore: update tests --- src/renderer/src/routes/_Map.test.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/routes/_Map.test.tsx b/src/renderer/src/routes/_Map.test.tsx index b490ea1..755fb43 100644 --- a/src/renderer/src/routes/_Map.test.tsx +++ b/src/renderer/src/routes/_Map.test.tsx @@ -6,17 +6,18 @@ import { describe, expect, test, vi } from 'vitest' import { IntlProvider } from '../contexts/IntlContext' import { routeTree } from '../routeTree.gen' -vi.mock('../contexts/persistedState/PersistedProjectId', () => ({ - ...vi.importActual('../contexts/persistedState/PersistedProjectId'), - usePersistedProjectIdStore: vi.fn((selector) => { - // Provide the mocked store state here - const mockedState = { - projectId: 'mocked-project-id', - setProjectId: vi.fn(), - } - return selector(mockedState) - }), -})) +vi.mock('../contexts/persistedState/PersistedProjectId', () => { + return { + usePersistedProjectIdStore: vi.fn((selector) => { + // Provide the mocked store state here + const mockedState = { + projectId: 'mocked-project-id', + setProjectId: vi.fn(), + } + return selector(mockedState) + }), + } +}) vi.mock('@comapeo/core-react', () => ({ useManyDocs: vi.fn(() => ({ data: [] })), From 4ae3314218d45062397f18f8f71067fb6bfc81f9 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:03:46 -0800 Subject: [PATCH 18/19] chore: update navigation with update Route --- src/renderer/src/routes/Onboarding/JoinProjectScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/routes/Onboarding/JoinProjectScreen.tsx b/src/renderer/src/routes/Onboarding/JoinProjectScreen.tsx index b75f77f..e7124f8 100644 --- a/src/renderer/src/routes/Onboarding/JoinProjectScreen.tsx +++ b/src/renderer/src/routes/Onboarding/JoinProjectScreen.tsx @@ -62,7 +62,7 @@ function JoinProjectScreenComponent() { const handleJoin = () => { // TODO: Add logic to join project - navigate({ to: '/tab1' }) + navigate({ to: '/Tab1' }) } const topMenu = ( From 756d894ad2432f3f38f79d0c6a1e757fe6044efd Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Sat, 14 Dec 2024 12:09:07 -0800 Subject: [PATCH 19/19] chore: update testing to be easier to use --- src/renderer/src/App.tsx | 2 +- .../persistedState/PersistedProjectId.tsx | 2 ++ .../persistedState/createPersistedState.tsx | 19 +++++++--- src/renderer/src/routes/_Map.test.tsx | 36 ++++++++----------- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 30a2344..6e0f1c1 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -4,7 +4,7 @@ import { RouterProvider, createRouter } from '@tanstack/react-router' import { usePersistedProjectIdStore } from './contexts/persistedState/PersistedProjectId' import { routeTree } from './routeTree.gen' -const router = createRouter({ +export const router = createRouter({ routeTree, context: { hasDeviceName: undefined!, persistedProjectId: undefined! }, }) diff --git a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx index 5303b92..aef46d6 100644 --- a/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx +++ b/src/renderer/src/contexts/persistedState/PersistedProjectId.tsx @@ -15,4 +15,6 @@ const projectIdSlice: StateCreator = (set) => ({ export const { Provider: PersistedProjectIdProvider, useStoreHook: usePersistedProjectIdStore, + Context: PersistedProjectIdContext, + nonPersistedStore: nonPersistedProjectIdStore, } = createPersistedStoreWithProvider(projectIdSlice, 'ActiveProjectId') diff --git a/src/renderer/src/contexts/persistedState/createPersistedState.tsx b/src/renderer/src/contexts/persistedState/createPersistedState.tsx index 2c32b0d..2d9cae4 100644 --- a/src/renderer/src/contexts/persistedState/createPersistedState.tsx +++ b/src/renderer/src/contexts/persistedState/createPersistedState.tsx @@ -4,15 +4,26 @@ import { persist } from 'zustand/middleware' type PersistedStoreKey = 'ActiveProjectId' +/** + * @param slice The shape of you store including it inital values + * @param persistedStoreKey A string used by local storage to index your store + * (must strongly typed in `type PersistedStoreKey`) + * + * @returns A Provider to be used by the app, a hook to consume a provider, and + * a context and non persisted store that can be used for testing + */ export function createPersistedStoreWithProvider( slice: StateCreator, persistedStoreKey: PersistedStoreKey, ) { - const store = createPersistedStore(slice, persistedStoreKey) - const Context = createContext(null) + const persistedStore = createPersistedStore(slice, persistedStoreKey) + // used for testing and injecting values into testing environment + const nonPersistedStore = createStore(slice) + // type persistedStore is a subset type of type nonPersistedStore + const Context = createContext(null) const Provider = ({ children }: { children: ReactNode }) => { - const [storeInstance] = useState(() => store) + const [storeInstance] = useState(() => persistedStore) return {children} } @@ -30,7 +41,7 @@ export function createPersistedStoreWithProvider( return useStore(contextStore, selector) } - return { Provider, useStoreHook } + return { Provider, useStoreHook, Context, nonPersistedStore } } function createPersistedStore( diff --git a/src/renderer/src/routes/_Map.test.tsx b/src/renderer/src/routes/_Map.test.tsx index 755fb43..525871a 100644 --- a/src/renderer/src/routes/_Map.test.tsx +++ b/src/renderer/src/routes/_Map.test.tsx @@ -1,40 +1,32 @@ import type { ReactNode } from 'react' -import { RouterProvider, createRouter } from '@tanstack/react-router' import { render, screen } from '@testing-library/react' import { describe, expect, test, vi } from 'vitest' +import { App, router } from '../App' import { IntlProvider } from '../contexts/IntlContext' -import { routeTree } from '../routeTree.gen' - -vi.mock('../contexts/persistedState/PersistedProjectId', () => { - return { - usePersistedProjectIdStore: vi.fn((selector) => { - // Provide the mocked store state here - const mockedState = { - projectId: 'mocked-project-id', - setProjectId: vi.fn(), - } - return selector(mockedState) - }), - } -}) +import { + PersistedProjectIdContext, + nonPersistedProjectIdStore, +} from '../contexts/persistedState/PersistedProjectId' + +nonPersistedProjectIdStore.setState(() => ({ + projectId: 'tester', +})) vi.mock('@comapeo/core-react', () => ({ useManyDocs: vi.fn(() => ({ data: [] })), + useOwnDeviceInfo: vi.fn(() => ({ data: { name: 'erik' } })), })) const Wrapper = ({ children }: { children: ReactNode }) => ( - {children} + + {children} + ) -const router = createRouter({ - routeTree, - context: { hasDeviceName: true, persistedProjectId: true }, -}) - describe('clicking tabs navigate to correct tab', () => { router.navigate({ to: '/Tab1' }) - render(, { wrapper: Wrapper }) + render(, { wrapper: Wrapper }) test('There are 4 tabs', async () => { const AllTabs = await screen.findAllByRole('tab')