Refactor to just CommandBar and GlobalState (#337)
* Refactor to just CommandBar and GlobalState * @Irev-Dev review: consolidate uses of useContext
This commit is contained in:
@ -2,7 +2,8 @@ import { render, screen } from '@testing-library/react'
|
|||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
import { describe, test, vi } from 'vitest'
|
import { describe, test, vi } from 'vitest'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { GlobalStateProvider } from './hooks/useAuthMachine'
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
|
|
||||||
let listener: ((rect: any) => void) | undefined = undefined
|
let listener: ((rect: any) => void) | undefined = undefined
|
||||||
;(global as any).ResizeObserver = class ResizeObserver {
|
;(global as any).ResizeObserver = class ResizeObserver {
|
||||||
@ -43,7 +44,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
|||||||
// wrap in router and xState context
|
// wrap in router and xState context
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<CommandBarProvider>
|
||||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
15
src/App.tsx
15
src/App.tsx
@ -5,7 +5,6 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useCallback,
|
useCallback,
|
||||||
MouseEventHandler,
|
MouseEventHandler,
|
||||||
useContext,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { DebugPanel } from './components/DebugPanel'
|
import { DebugPanel } from './components/DebugPanel'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
@ -49,8 +48,7 @@ import { writeTextFile } from '@tauri-apps/api/fs'
|
|||||||
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
||||||
import { IndexLoaderData } from './Router'
|
import { IndexLoaderData } from './Router'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { useAuthMachine } from './hooks/useAuthMachine'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { SettingsContext } from 'components/SettingsCommandProvider'
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||||
@ -129,10 +127,15 @@ export function App() {
|
|||||||
setStreamDimensions: s.setStreamDimensions,
|
setStreamDimensions: s.setStreamDimensions,
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
}))
|
}))
|
||||||
const { showDebugPanel, theme, onboardingStatus } =
|
|
||||||
useContext(SettingsContext)
|
|
||||||
|
|
||||||
const [token] = useAuthMachine((s) => s?.context?.token)
|
const {
|
||||||
|
auth: {
|
||||||
|
context: { token },
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
context: { showDebugPanel, theme, onboardingStatus },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import Loading from './components/Loading'
|
import Loading from './components/Loading'
|
||||||
import { useAuthMachine } from './hooks/useAuthMachine'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
// Wrapper around protected routes, used in src/Router.tsx
|
// Wrapper around protected routes, used in src/Router.tsx
|
||||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||||
const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn'))
|
const {
|
||||||
|
auth: { state },
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
const isLoggedIn = state.matches('checkIfLoggedIn')
|
||||||
|
|
||||||
return isLoggedIn ? (
|
return isLoggedIn ? (
|
||||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||||
|
@ -24,16 +24,13 @@ import {
|
|||||||
} from './lib/tauriFS'
|
} from './lib/tauriFS'
|
||||||
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
import DownloadAppBanner from './components/DownloadAppBanner'
|
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||||
import {
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
AuthMachineCommandProvider,
|
|
||||||
GlobalStateProvider,
|
|
||||||
} from './hooks/useAuthMachine'
|
|
||||||
import SettingsCommandProvider from './components/SettingsCommandProvider'
|
|
||||||
import {
|
import {
|
||||||
SETTINGS_PERSIST_KEY,
|
SETTINGS_PERSIST_KEY,
|
||||||
settingsMachine,
|
settingsMachine,
|
||||||
} from './machines/settingsMachine'
|
} from './machines/settingsMachine'
|
||||||
import { ContextFrom } from 'xstate'
|
import { ContextFrom } from 'xstate'
|
||||||
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
|
|
||||||
const prependRoutes =
|
const prependRoutes =
|
||||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||||
@ -78,13 +75,9 @@ const addGlobalContextToElements = (
|
|||||||
? {
|
? {
|
||||||
...route,
|
...route,
|
||||||
element: (
|
element: (
|
||||||
<GlobalStateProvider>
|
<CommandBarProvider>
|
||||||
<AuthMachineCommandProvider>
|
<GlobalStateProvider>{route.element}</GlobalStateProvider>
|
||||||
<SettingsCommandProvider>
|
</CommandBarProvider>
|
||||||
{route.element}
|
|
||||||
</SettingsCommandProvider>
|
|
||||||
</AuthMachineCommandProvider>
|
|
||||||
</GlobalStateProvider>
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: route
|
: route
|
||||||
|
@ -2,7 +2,7 @@ import { Toolbar } from '../Toolbar'
|
|||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
@ -18,7 +18,11 @@ export const AppHeader = ({
|
|||||||
className = '',
|
className = '',
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const [user] = useAuthMachine((s) => s?.context?.user)
|
const {
|
||||||
|
auth: {
|
||||||
|
context: { user },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
Fragment,
|
Fragment,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
@ -12,6 +11,7 @@ import { ActionIcon } from './ActionIcon'
|
|||||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { Command, SubCommand } from '../lib/commands'
|
import { Command, SubCommand } from '../lib/commands'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
|
||||||
export type SortedCommand = {
|
export type SortedCommand = {
|
||||||
item: Partial<Command | SubCommand> & { name: string }
|
item: Partial<Command | SubCommand> & { name: string }
|
||||||
@ -27,9 +27,41 @@ export const CommandsContext = createContext(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const CommandBarProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const [commands, internalSetCommands] = useState([] as Command[])
|
||||||
|
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
||||||
|
|
||||||
|
const addCommands = (newCommands: Command[]) => {
|
||||||
|
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
||||||
|
}
|
||||||
|
const removeCommands = (newCommands: Command[]) => {
|
||||||
|
internalSetCommands((prevCommands) =>
|
||||||
|
prevCommands.filter((command) => !newCommands.includes(command))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandsContext.Provider
|
||||||
|
value={{
|
||||||
|
commands,
|
||||||
|
addCommands,
|
||||||
|
removeCommands,
|
||||||
|
commandBarOpen,
|
||||||
|
setCommandBarOpen,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CommandBar />
|
||||||
|
</CommandsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CommandBar = () => {
|
const CommandBar = () => {
|
||||||
const { commands, commandBarOpen, setCommandBarOpen } =
|
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
||||||
useContext(CommandsContext)
|
|
||||||
useHotkeys('meta+k', () => {
|
useHotkeys('meta+k', () => {
|
||||||
if (commands.length === 0) return
|
if (commands.length === 0) return
|
||||||
setCommandBarOpen(!commandBarOpen)
|
setCommandBarOpen(!commandBarOpen)
|
||||||
@ -255,4 +287,4 @@ const CommandBar = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommandBar
|
export default CommandBarProvider
|
||||||
|
147
src/components/GlobalStateProvider.tsx
Normal file
147
src/components/GlobalStateProvider.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { paths } from '../Router'
|
||||||
|
import {
|
||||||
|
authCommandBarMeta,
|
||||||
|
authMachine,
|
||||||
|
TOKEN_PERSIST_KEY,
|
||||||
|
} from '../machines/authMachine'
|
||||||
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
|
import React, { createContext, useEffect, useRef } from 'react'
|
||||||
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
|
import {
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
settingsCommandBarMeta,
|
||||||
|
settingsMachine,
|
||||||
|
} from 'machines/settingsMachine'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { setThemeClass } from 'lib/theme'
|
||||||
|
import {
|
||||||
|
AnyStateMachine,
|
||||||
|
ContextFrom,
|
||||||
|
InterpreterFrom,
|
||||||
|
Prop,
|
||||||
|
StateFrom,
|
||||||
|
} from 'xstate'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
|
||||||
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
|
state: StateFrom<T>
|
||||||
|
context: ContextFrom<T>
|
||||||
|
send: Prop<InterpreterFrom<T>, 'send'>
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlobalContext = {
|
||||||
|
auth: MachineContext<typeof authMachine>
|
||||||
|
settings: MachineContext<typeof settingsMachine>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalStateContext = createContext({} as GlobalContext)
|
||||||
|
|
||||||
|
export const GlobalStateProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { commands } = useCommandsContext()
|
||||||
|
|
||||||
|
// Settings machine setup
|
||||||
|
const retrievedSettings = useRef(
|
||||||
|
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
|
||||||
|
)
|
||||||
|
const persistedSettings = Object.assign(
|
||||||
|
settingsMachine.initialState.context,
|
||||||
|
JSON.parse(retrievedSettings.current) as Partial<
|
||||||
|
(typeof settingsMachine)['context']
|
||||||
|
>
|
||||||
|
)
|
||||||
|
|
||||||
|
const [settingsState, settingsSend] = useMachine(settingsMachine, {
|
||||||
|
context: persistedSettings,
|
||||||
|
actions: {
|
||||||
|
toastSuccess: (context, event) => {
|
||||||
|
const truncatedNewValue =
|
||||||
|
'data' in event && event.data instanceof Object
|
||||||
|
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
||||||
|
.toString()
|
||||||
|
.substring(0, 28) as any)
|
||||||
|
: undefined
|
||||||
|
toast.success(
|
||||||
|
event.type +
|
||||||
|
(truncatedNewValue
|
||||||
|
? ` to "${truncatedNewValue}${
|
||||||
|
truncatedNewValue.length === 28 ? '...' : ''
|
||||||
|
}"`
|
||||||
|
: '')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useStateMachineCommands({
|
||||||
|
state: settingsState,
|
||||||
|
send: settingsSend,
|
||||||
|
commands,
|
||||||
|
owner: 'settings',
|
||||||
|
commandBarMeta: settingsCommandBarMeta,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => setThemeClass(settingsState.context.theme),
|
||||||
|
[settingsState.context.theme]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auth machine setup
|
||||||
|
const [authState, authSend] = useMachine(authMachine, {
|
||||||
|
actions: {
|
||||||
|
goToSignInPage: () => {
|
||||||
|
navigate(paths.SIGN_IN)
|
||||||
|
logout()
|
||||||
|
},
|
||||||
|
goToIndexPage: () => {
|
||||||
|
if (window.location.pathname.includes(paths.SIGN_IN)) {
|
||||||
|
navigate(paths.INDEX)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useStateMachineCommands({
|
||||||
|
state: authState,
|
||||||
|
send: authSend,
|
||||||
|
commands,
|
||||||
|
commandBarMeta: authCommandBarMeta,
|
||||||
|
owner: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GlobalStateContext.Provider
|
||||||
|
value={{
|
||||||
|
auth: {
|
||||||
|
state: authState,
|
||||||
|
context: authState.context,
|
||||||
|
send: authSend,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
state: settingsState,
|
||||||
|
context: settingsState.context,
|
||||||
|
send: settingsSend,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</GlobalStateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalStateProvider
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
const url = withBaseUrl('/logout')
|
||||||
|
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
}
|
@ -1,76 +0,0 @@
|
|||||||
import {
|
|
||||||
SETTINGS_PERSIST_KEY,
|
|
||||||
settingsCommandBarMeta,
|
|
||||||
settingsMachine,
|
|
||||||
} from '../machines/settingsMachine'
|
|
||||||
import { useMachine } from '@xstate/react'
|
|
||||||
import { CommandsContext } from './CommandBar'
|
|
||||||
import { createContext, useContext, useEffect, useRef } from 'react'
|
|
||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
|
||||||
import { setThemeClass } from '../lib/theme'
|
|
||||||
import { ContextFrom, InterpreterFrom, Prop } from 'xstate'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
|
|
||||||
export const SettingsContext = createContext(
|
|
||||||
{} as {
|
|
||||||
send: Prop<InterpreterFrom<typeof settingsMachine>, 'send'>
|
|
||||||
} & ContextFrom<typeof settingsMachine>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default function SettingsCommandProvider({
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren) {
|
|
||||||
const retrievedSettings = useRef(
|
|
||||||
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
|
|
||||||
)
|
|
||||||
const persistedSettings = Object.assign(
|
|
||||||
settingsMachine.initialState.context,
|
|
||||||
JSON.parse(retrievedSettings.current) as Partial<
|
|
||||||
(typeof settingsMachine)['context']
|
|
||||||
>
|
|
||||||
)
|
|
||||||
|
|
||||||
const [state, send] = useMachine(settingsMachine, {
|
|
||||||
context: persistedSettings,
|
|
||||||
actions: {
|
|
||||||
toastSuccess: (context, event) => {
|
|
||||||
const truncatedNewValue =
|
|
||||||
'data' in event && event.data instanceof Object
|
|
||||||
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
|
||||||
.toString()
|
|
||||||
.substring(0, 28) as any)
|
|
||||||
: undefined
|
|
||||||
toast.success(
|
|
||||||
event.type +
|
|
||||||
(truncatedNewValue
|
|
||||||
? ` to "${truncatedNewValue}${
|
|
||||||
truncatedNewValue.length === 28 ? '...' : ''
|
|
||||||
}"`
|
|
||||||
: '')
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const { commands } = useContext(CommandsContext)
|
|
||||||
|
|
||||||
useStateMachineCommands({
|
|
||||||
state,
|
|
||||||
send,
|
|
||||||
commands,
|
|
||||||
owner: 'settings',
|
|
||||||
commandBarMeta: settingsCommandBarMeta,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => setThemeClass(state.context.theme), [state.context.theme])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsContext.Provider
|
|
||||||
value={{
|
|
||||||
send,
|
|
||||||
...state.context,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SettingsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,7 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
|||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { GlobalStateProvider } from '../hooks/useAuthMachine'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
|
import CommandBarProvider from './CommandBar'
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
@ -94,7 +95,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
|||||||
// wrap in router and xState context
|
// wrap in router and xState context
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<CommandBarProvider>
|
||||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
||||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
@ -15,7 +15,9 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
const displayedName = getDisplayName(user)
|
const displayedName = getDisplayName(user)
|
||||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [_, send] = useAuthMachine()
|
const {
|
||||||
|
auth: { send },
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
// Fallback logic for displaying user's "name":
|
// Fallback logic for displaying user's "name":
|
||||||
// 1. user.name
|
// 1. user.name
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
import { createActorContext } from '@xstate/react'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { paths } from '../Router'
|
|
||||||
import {
|
|
||||||
authCommandBarMeta,
|
|
||||||
authMachine,
|
|
||||||
TOKEN_PERSIST_KEY,
|
|
||||||
} from '../machines/authMachine'
|
|
||||||
import withBaseUrl from '../lib/withBaseURL'
|
|
||||||
import React, { useContext, useState } from 'react'
|
|
||||||
import CommandBar, { CommandsContext } from '../components/CommandBar'
|
|
||||||
import { Command } from '../lib/commands'
|
|
||||||
import useStateMachineCommands from './useStateMachineCommands'
|
|
||||||
|
|
||||||
export const AuthMachineContext = createActorContext(authMachine)
|
|
||||||
|
|
||||||
export const GlobalStateProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const [commands, internalSetCommands] = useState([] as Command[])
|
|
||||||
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const addCommands = (newCommands: Command[]) => {
|
|
||||||
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
|
||||||
}
|
|
||||||
const removeCommands = (newCommands: Command[]) => {
|
|
||||||
internalSetCommands((prevCommands) =>
|
|
||||||
prevCommands.filter((command) => !newCommands.includes(command))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthMachineContext.Provider
|
|
||||||
machine={() =>
|
|
||||||
authMachine.withConfig({
|
|
||||||
actions: {
|
|
||||||
goToSignInPage: () => {
|
|
||||||
navigate(paths.SIGN_IN)
|
|
||||||
logout()
|
|
||||||
},
|
|
||||||
goToIndexPage: () => {
|
|
||||||
if (window.location.pathname.includes(paths.SIGN_IN)) {
|
|
||||||
navigate(paths.INDEX)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CommandsContext.Provider
|
|
||||||
value={{
|
|
||||||
commands,
|
|
||||||
addCommands,
|
|
||||||
removeCommands,
|
|
||||||
commandBarOpen,
|
|
||||||
setCommandBarOpen,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<CommandBar />
|
|
||||||
</CommandsContext.Provider>
|
|
||||||
</AuthMachineContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAuthMachine<T>(
|
|
||||||
selector: (
|
|
||||||
state: Parameters<Parameters<typeof AuthMachineContext.useSelector>[0]>[0]
|
|
||||||
) => T = () => null as T
|
|
||||||
): [T, ReturnType<typeof AuthMachineContext.useActor>[1]] {
|
|
||||||
// useActor api normally `[state, send] = useActor`
|
|
||||||
// we're only interested in send because of the selector
|
|
||||||
const send = AuthMachineContext.useActor()[1]
|
|
||||||
|
|
||||||
const selection = AuthMachineContext.useSelector(selector)
|
|
||||||
return [selection, send]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logout() {
|
|
||||||
const url = withBaseUrl('/logout')
|
|
||||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
|
||||||
return fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthMachineCommandProvider(props: React.PropsWithChildren<{}>) {
|
|
||||||
const [state, send] = AuthMachineContext.useActor()
|
|
||||||
const { commands } = useContext(CommandsContext)
|
|
||||||
|
|
||||||
useStateMachineCommands({
|
|
||||||
state,
|
|
||||||
send,
|
|
||||||
commands,
|
|
||||||
commandBarMeta: authCommandBarMeta,
|
|
||||||
owner: 'auth',
|
|
||||||
})
|
|
||||||
|
|
||||||
return <>{props.children}</>
|
|
||||||
}
|
|
6
src/hooks/useCommandsContext.ts
Normal file
6
src/hooks/useCommandsContext.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { CommandsContext } from 'components/CommandBar'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
export const useCommandsContext = () => {
|
||||||
|
return useContext(CommandsContext)
|
||||||
|
}
|
6
src/hooks/useGlobalStateContext.ts
Normal file
6
src/hooks/useGlobalStateContext.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { GlobalStateContext } from 'components/GlobalStateProvider'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
export const useGlobalStateContext = () => {
|
||||||
|
return useContext(GlobalStateContext)
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { useContext, useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||||
import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands'
|
import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands'
|
||||||
import { CommandsContext } from '../components/CommandBar'
|
import { useCommandsContext } from './useCommandsContext'
|
||||||
|
|
||||||
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -17,7 +17,7 @@ export default function useStateMachineCommands<T extends AnyStateMachine>({
|
|||||||
commandBarMeta,
|
commandBarMeta,
|
||||||
owner,
|
owner,
|
||||||
}: UseStateMachineCommandsArgs<T>) {
|
}: UseStateMachineCommandsArgs<T>) {
|
||||||
const { addCommands, removeCommands } = useContext(CommandsContext)
|
const { addCommands, removeCommands } = useCommandsContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newCommands = state.nextEvents
|
const newCommands = state.nextEvents
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
export default function fetcher(input: RequestInfo, init: RequestInit = {}) {
|
||||||
|
const fetcherWithToken = async (token?: string): Promise<JSON> => {
|
||||||
export default async function fetcher<JSON = any>(
|
|
||||||
input: RequestInfo,
|
|
||||||
init: RequestInit = {}
|
|
||||||
): Promise<JSON> {
|
|
||||||
const [token] = useAuthMachine((s) => s?.context?.token)
|
|
||||||
const headers = { ...init.headers } as Record<string, string>
|
const headers = { ...init.headers } as Record<string, string>
|
||||||
if (token) {
|
if (token) {
|
||||||
headers.Authorization = `Bearer ${token}`
|
headers.Authorization = `Bearer ${token}`
|
||||||
@ -13,4 +8,6 @@ export default async function fetcher<JSON = any>(
|
|||||||
const credentials = 'include' as RequestCredentials
|
const credentials = 'include' as RequestCredentials
|
||||||
const res = await fetch(input, { ...init, credentials, headers })
|
const res = await fetch(input, { ...init, credentials, headers })
|
||||||
return res.json()
|
return res.json()
|
||||||
|
}
|
||||||
|
return fetcherWithToken
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FormEvent, useContext, useEffect } from 'react'
|
import { FormEvent, useEffect } from 'react'
|
||||||
import { removeDir, renameFile } from '@tauri-apps/api/fs'
|
import { removeDir, renameFile } from '@tauri-apps/api/fs'
|
||||||
import {
|
import {
|
||||||
createNewProject,
|
createNewProject,
|
||||||
@ -25,17 +25,21 @@ import {
|
|||||||
getSortFunction,
|
getSortFunction,
|
||||||
getSortIcon,
|
getSortIcon,
|
||||||
} from '../lib/sorting'
|
} from '../lib/sorting'
|
||||||
import { CommandsContext } from '../components/CommandBar'
|
|
||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
import { SettingsContext } from '../components/SettingsCommandProvider'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
|
||||||
// This route only opens in the Tauri desktop context for now,
|
// This route only opens in the Tauri desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { commands, setCommandBarOpen } = useContext(CommandsContext)
|
const { commands, setCommandBarOpen } = useCommandsContext()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
||||||
const { defaultDirectory, defaultProjectName } = useContext(SettingsContext)
|
const {
|
||||||
|
settings: {
|
||||||
|
context: { defaultDirectory, defaultProjectName },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
const [state, send] = useMachine(homeMachine, {
|
const [state, send] = useMachine(homeMachine, {
|
||||||
context: {
|
context: {
|
||||||
|
@ -3,14 +3,19 @@ import { BaseUnit, baseUnits } from '../../useStore'
|
|||||||
import { ActionButton } from '../../components/ActionButton'
|
import { ActionButton } from '../../components/ActionButton'
|
||||||
import { SettingsSection } from '../Settings'
|
import { SettingsSection } from '../Settings'
|
||||||
import { Toggle } from '../../components/Toggle/Toggle'
|
import { Toggle } from '../../components/Toggle/Toggle'
|
||||||
import { useContext, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||||
import { SettingsContext } from '../../components/SettingsCommandProvider'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
export default function Units() {
|
export default function Units() {
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.CAMERA)
|
const next = useNextClick(onboardingPaths.CAMERA)
|
||||||
const { send, unitSystem, baseUnit } = useContext(SettingsContext)
|
const {
|
||||||
|
settings: {
|
||||||
|
send,
|
||||||
|
context: { unitSystem, baseUnit },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
const [tempUnitSystem, setTempUnitSystem] = useState(unitSystem)
|
const [tempUnitSystem, setTempUnitSystem] = useState(unitSystem)
|
||||||
const [tempBaseUnit, setTempBaseUnit] = useState(baseUnit)
|
const [tempBaseUnit, setTempBaseUnit] = useState(baseUnit)
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ import Introduction from './Introduction'
|
|||||||
import Units from './Units'
|
import Units from './Units'
|
||||||
import Camera from './Camera'
|
import Camera from './Camera'
|
||||||
import Sketching from './Sketching'
|
import Sketching from './Sketching'
|
||||||
import { useCallback, useContext } from 'react'
|
import { useCallback } from 'react'
|
||||||
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
|
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
|
||||||
import { SettingsContext } from '../../components/SettingsCommandProvider'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
export const onboardingPaths = {
|
export const onboardingPaths = {
|
||||||
INDEX: '/',
|
INDEX: '/',
|
||||||
@ -35,7 +35,9 @@ export const onboardingRoutes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export function useNextClick(newStatus: string) {
|
export function useNextClick(newStatus: string) {
|
||||||
const { send } = useContext(SettingsContext)
|
const {
|
||||||
|
settings: { send },
|
||||||
|
} = useGlobalStateContext()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
@ -48,7 +50,9 @@ export function useNextClick(newStatus: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useDismiss() {
|
export function useDismiss() {
|
||||||
const { send } = useContext(SettingsContext)
|
const {
|
||||||
|
settings: { send },
|
||||||
|
} = useGlobalStateContext()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
|
@ -7,27 +7,32 @@ import { ActionButton } from '../components/ActionButton'
|
|||||||
import { AppHeader } from '../components/AppHeader'
|
import { AppHeader } from '../components/AppHeader'
|
||||||
import { open } from '@tauri-apps/api/dialog'
|
import { open } from '@tauri-apps/api/dialog'
|
||||||
import { BaseUnit, baseUnits } from '../useStore'
|
import { BaseUnit, baseUnits } from '../useStore'
|
||||||
import { useContext } from 'react'
|
|
||||||
import { Toggle } from '../components/Toggle/Toggle'
|
import { Toggle } from '../components/Toggle/Toggle'
|
||||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { IndexLoaderData, paths } from '../Router'
|
import { IndexLoaderData, paths } from '../Router'
|
||||||
import { Themes } from '../lib/theme'
|
import { Themes } from '../lib/theme'
|
||||||
import { SettingsContext } from '../components/SettingsCommandProvider'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
useHotkeys('esc', () => navigate('../'))
|
useHotkeys('esc', () => navigate('../'))
|
||||||
const {
|
const {
|
||||||
|
settings: {
|
||||||
|
send,
|
||||||
|
state: {
|
||||||
|
context: {
|
||||||
defaultProjectName,
|
defaultProjectName,
|
||||||
showDebugPanel,
|
showDebugPanel,
|
||||||
defaultDirectory,
|
defaultDirectory,
|
||||||
unitSystem,
|
unitSystem,
|
||||||
baseUnit,
|
baseUnit,
|
||||||
theme,
|
theme,
|
||||||
send,
|
},
|
||||||
} = useContext(SettingsContext)
|
},
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
async function handleDirectorySelection() {
|
async function handleDirectorySelection() {
|
||||||
const newDirectory = await open({
|
const newDirectory = await open({
|
||||||
|
@ -5,13 +5,18 @@ import { invoke } from '@tauri-apps/api/tauri'
|
|||||||
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
||||||
import { Themes, getSystemTheme } from '../lib/theme'
|
import { Themes, getSystemTheme } from '../lib/theme'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { useContext } from 'react'
|
|
||||||
import { SettingsContext } from 'components/SettingsCommandProvider'
|
|
||||||
|
|
||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
const { theme } = useContext(SettingsContext)
|
const {
|
||||||
const [_, send] = useAuthMachine()
|
auth: { send },
|
||||||
|
settings: {
|
||||||
|
state: {
|
||||||
|
context: { theme },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
|
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
const signInTauri = async () => {
|
const signInTauri = async () => {
|
||||||
// We want to invoke our command to login via device auth.
|
// We want to invoke our command to login via device auth.
|
||||||
|
Reference in New Issue
Block a user