Xstate Auth migration (#250)
* auth migrate progress, web only * wrap home in state provider * use consistent logged spelling * use createActorContext * typo * fix wraping problem
This commit is contained in:
@ -19,6 +19,7 @@
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.15.1",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"formik": "^2.4.3",
|
||||
"http-server": "^14.1.1",
|
||||
@ -42,6 +43,7 @@
|
||||
"wasm-pack": "^0.12.1",
|
||||
"web-vitals": "^2.1.0",
|
||||
"ws": "^8.13.0",
|
||||
"xstate": "^4.38.2",
|
||||
"zustand": "^4.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'
|
||||
import { App } from './App'
|
||||
import { describe, test, vi } from 'vitest'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './hooks/useAuthMachine'
|
||||
|
||||
let listener: ((rect: any) => void) | undefined = undefined
|
||||
;(global as any).ResizeObserver = class ResizeObserver {
|
||||
@ -27,9 +28,9 @@ describe('App tests', () => {
|
||||
}
|
||||
})
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<TestWrap>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</TestWrap>
|
||||
)
|
||||
const linkElement = screen.getByText(/Variables/i)
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
@ -37,3 +38,12 @@ describe('App tests', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ import { writeTextFile } from '@tauri-apps/api/fs'
|
||||
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
||||
import { IndexLoaderData } from './Router'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useAuthMachine } from './hooks/useAuthMachine'
|
||||
|
||||
export function App() {
|
||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||
@ -81,7 +82,6 @@ export function App() {
|
||||
isMouseDownInStream,
|
||||
cmdId,
|
||||
setCmdId,
|
||||
token,
|
||||
formatCode,
|
||||
debugPanel,
|
||||
theme,
|
||||
@ -121,7 +121,6 @@ export function App() {
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
cmdId: s.cmdId,
|
||||
setCmdId: s.setCmdId,
|
||||
token: s.token,
|
||||
formatCode: s.formatCode,
|
||||
debugPanel: s.debugPanel,
|
||||
addKCLError: s.addKCLError,
|
||||
@ -134,6 +133,7 @@ export function App() {
|
||||
setStreamDimensions: s.setStreamDimensions,
|
||||
streamDimensions: s.streamDimensions,
|
||||
}))
|
||||
const [token] = useAuthMachine((s) => s?.context?.token)
|
||||
|
||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||
|
||||
|
33
src/Auth.tsx
33
src/Auth.tsx
@ -1,38 +1,11 @@
|
||||
import useSWR from 'swr'
|
||||
import fetcher from './lib/fetcher'
|
||||
import withBaseUrl from './lib/withBaseURL'
|
||||
import { User, useStore } from './useStore'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useEffect } from 'react'
|
||||
import { isTauri } from './lib/isTauri'
|
||||
import Loading from './components/Loading'
|
||||
import { paths } from './Router'
|
||||
import { useAuthMachine } from './hooks/useAuthMachine'
|
||||
|
||||
// Wrapper around protected routes, used in src/Router.tsx
|
||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||
const { data: user, isLoading } = useSWR<
|
||||
User | Partial<{ error_code: string }>
|
||||
>(withBaseUrl('/user'), fetcher)
|
||||
const { token, setUser } = useStore((s) => ({
|
||||
token: s.token,
|
||||
setUser: s.setUser,
|
||||
}))
|
||||
const navigate = useNavigate()
|
||||
const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn'))
|
||||
|
||||
useEffect(() => {
|
||||
if (user && 'id' in user) setUser(user)
|
||||
}, [user, setUser])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(isTauri() && !token) ||
|
||||
(!isTauri() && !isLoading && !(user && 'id' in user))
|
||||
) {
|
||||
navigate(paths.SIGN_IN)
|
||||
}
|
||||
}, [user, token, navigate, isLoading])
|
||||
|
||||
return isLoading ? (
|
||||
return isLoggedIn ? (
|
||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||
) : (
|
||||
<>{children}</>
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
} from './lib/tauriFS'
|
||||
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||
import { GlobalStateProvider } from './hooks/useAuthMachine'
|
||||
|
||||
const prependRoutes =
|
||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||
@ -58,7 +59,22 @@ export type HomeLoaderData = {
|
||||
projects: ProjectWithEntryPointMetadata[]
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
||||
|
||||
const addGlobalContextToElements = (
|
||||
routes: CreateBrowserRouterArg
|
||||
): CreateBrowserRouterArg =>
|
||||
routes.map((route) =>
|
||||
'element' in route
|
||||
? {
|
||||
...route,
|
||||
element: <GlobalStateProvider>{route.element}</GlobalStateProvider>,
|
||||
}
|
||||
: route
|
||||
)
|
||||
|
||||
const router = createBrowserRouter(
|
||||
addGlobalContextToElements([
|
||||
{
|
||||
path: paths.INDEX,
|
||||
loader: () =>
|
||||
@ -95,7 +111,9 @@ const router = createBrowserRouter([
|
||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||
|
||||
if (shouldRedirectToOnboarding) {
|
||||
return redirect(makeUrlPathRelative(paths.ONBOARDING.INDEX) + status)
|
||||
return redirect(
|
||||
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +193,8 @@ const router = createBrowserRouter([
|
||||
path: paths.SIGN_IN,
|
||||
element: <SignIn />,
|
||||
},
|
||||
])
|
||||
])
|
||||
)
|
||||
|
||||
/**
|
||||
* All routes in the app, used in src/index.tsx
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Toolbar } from '../Toolbar'
|
||||
import { useStore } from '../useStore'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
@ -18,9 +18,7 @@ export const AppHeader = ({
|
||||
className = '',
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const { user } = useStore((s) => ({
|
||||
user: s.user,
|
||||
}))
|
||||
const [user] = useAuthMachine((s) => s?.context?.user)
|
||||
|
||||
return (
|
||||
<header
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { User } from '../useStore'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { GlobalStateProvider } from '../hooks/useAuthMachine'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
describe('UserSidebarMenu tests', () => {
|
||||
test("Renders user's name and email if available", () => {
|
||||
@ -12,12 +15,18 @@ describe('UserSidebarMenu tests', () => {
|
||||
image: 'https://placekitten.com/200/200',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
company: 'Test Company',
|
||||
discord: 'Test User#1234',
|
||||
github: 'testuser',
|
||||
phone: '555-555-5555',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
}
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<TestWrap>
|
||||
<UserSidebarMenu user={userWellFormed} />
|
||||
</BrowserRouter>
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||
@ -35,12 +44,19 @@ describe('UserSidebarMenu tests', () => {
|
||||
image: 'https://placekitten.com/200/200',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
company: 'Test Company',
|
||||
discord: 'Test User#1234',
|
||||
github: 'testuser',
|
||||
phone: '555-555-5555',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
name: '',
|
||||
}
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<TestWrap>
|
||||
<UserSidebarMenu user={userNoName} />
|
||||
</BrowserRouter>
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||
@ -55,14 +71,30 @@ describe('UserSidebarMenu tests', () => {
|
||||
email: 'kittycad.sidebar.test@example.com',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
company: 'Test Company',
|
||||
discord: 'Test User#1234',
|
||||
github: 'testuser',
|
||||
phone: '555-555-5555',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
image: '',
|
||||
}
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<TestWrap>
|
||||
<UserSidebarMenu user={userNoAvatar} />
|
||||
</BrowserRouter>
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu')
|
||||
})
|
||||
})
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { User, useStore } from '../useStore'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
@ -7,14 +6,16 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
import { paths } from '../Router'
|
||||
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||
import { Models } from '@kittycad/lib'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
const displayedName = getDisplayName(user)
|
||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const { setToken } = useStore((s) => ({
|
||||
setToken: s.setToken,
|
||||
}))
|
||||
const [_, send] = useAuthMachine()
|
||||
|
||||
// Fallback logic for displaying user's "name":
|
||||
// 1. user.name
|
||||
@ -119,10 +120,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
setToken('')
|
||||
navigate(paths.SIGN_IN)
|
||||
}}
|
||||
onClick={() => send('logout')}
|
||||
icon={{
|
||||
icon: faSignOutAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
|
54
src/hooks/useAuthMachine.tsx
Normal file
54
src/hooks/useAuthMachine.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { createActorContext } from '@xstate/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from '../Router'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../lib/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
|
||||
export const AuthMachineContext = createActorContext(authMachine)
|
||||
|
||||
export const GlobalStateProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<AuthMachineContext.Provider
|
||||
machine={() =>
|
||||
authMachine.withConfig({
|
||||
actions: {
|
||||
goToSignInPage: () => {
|
||||
navigate(paths.SIGN_IN)
|
||||
logout()
|
||||
},
|
||||
goToIndexPage: () => navigate(paths.INDEX),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</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',
|
||||
})
|
||||
}
|
@ -363,7 +363,6 @@ export class EngineCommandManager {
|
||||
this.onHoverCallback = callback
|
||||
}
|
||||
onClick(callback: (selection?: SelectionsArgs) => void) {
|
||||
// TODO talk to the gang about this
|
||||
// It's when the user clicks on a part in the 3d scene, and so the engine should tell the
|
||||
// frontend about that (with it's id) so that the FE can put the user's cursor on the right
|
||||
// line of code
|
||||
|
@ -136,11 +136,6 @@ export const lineTo: SketchLineHelper = {
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
// engineCommandManager.sendModellingCommand({
|
||||
// id,
|
||||
// params: [lineData, previousSketch],
|
||||
// range: sourceRange,
|
||||
// })
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
@ -673,11 +668,6 @@ export const angledLine: SketchLineHelper = {
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
// engineCommandManager.sendModellingCommand({
|
||||
// id,
|
||||
// params: [lineData, previousSketch],
|
||||
// range: sourceRange,
|
||||
// })
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
|
102
src/lib/authMachine.ts
Normal file
102
src/lib/authMachine.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { createMachine, assign } from 'xstate'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import withBaseURL from '../lib/withBaseURL'
|
||||
|
||||
export interface UserContext {
|
||||
user?: Models['User_type']
|
||||
token?: string
|
||||
}
|
||||
|
||||
export type Events =
|
||||
| {
|
||||
type: 'logout'
|
||||
}
|
||||
| {
|
||||
type: 'tryLogin'
|
||||
token?: string
|
||||
}
|
||||
|
||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||
|
||||
export const authMachine = createMachine<UserContext, Events>(
|
||||
{
|
||||
id: 'Auth',
|
||||
initial: 'checkIfLoggedIn',
|
||||
states: {
|
||||
checkIfLoggedIn: {
|
||||
id: 'check-if-logged-in',
|
||||
invoke: {
|
||||
src: 'getUser',
|
||||
id: 'check-logged-in',
|
||||
onDone: [
|
||||
{
|
||||
target: 'loggedIn',
|
||||
actions: assign({
|
||||
user: (context, event) => event.data,
|
||||
}),
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: 'loggedOut',
|
||||
actions: assign({
|
||||
user: () => undefined,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
loggedIn: {
|
||||
entry: ['goToIndexPage'],
|
||||
on: {
|
||||
logout: {
|
||||
target: 'loggedOut',
|
||||
},
|
||||
},
|
||||
},
|
||||
loggedOut: {
|
||||
entry: ['goToSignInPage'],
|
||||
on: {
|
||||
tryLogin: {
|
||||
target: 'checkIfLoggedIn',
|
||||
actions: assign({
|
||||
token: (context, event) => {
|
||||
const token = event.token || ''
|
||||
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
||||
return token
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
schema: { events: {} as { type: 'logout' } | { type: 'tryLogin' } },
|
||||
predictableActionArguments: true,
|
||||
preserveActionOrder: true,
|
||||
context: { token: persistedToken },
|
||||
},
|
||||
{
|
||||
actions: {},
|
||||
services: { getUser },
|
||||
guards: {},
|
||||
delays: {},
|
||||
}
|
||||
)
|
||||
|
||||
async function getUser(context: UserContext) {
|
||||
const url = withBaseURL('/user')
|
||||
const headers: { [key: string]: string } = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (!context.token && '__TAURI__' in window) throw 'not log in'
|
||||
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
})
|
||||
const user = await response.json()
|
||||
if ('error_code' in user) throw new Error(user.message)
|
||||
return user
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import { useStore } from '../useStore'
|
||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||
|
||||
export default async function fetcher<JSON = any>(
|
||||
input: RequestInfo,
|
||||
init: RequestInit = {}
|
||||
): Promise<JSON> {
|
||||
const { token } = useStore.getState()
|
||||
const [token] = useAuthMachine((s) => s?.context?.token)
|
||||
const headers = { ...init.headers } as Record<string, string>
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
|
@ -7,13 +7,14 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
||||
import { getSystemTheme } from '../lib/getSystemTheme'
|
||||
import { paths } from '../Router'
|
||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||
|
||||
const SignIn = () => {
|
||||
const navigate = useNavigate()
|
||||
const { setToken, theme } = useStore((s) => ({
|
||||
setToken: s.setToken,
|
||||
const { theme } = useStore((s) => ({
|
||||
theme: s.theme,
|
||||
}))
|
||||
const [_, send] = useAuthMachine()
|
||||
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||
const signInTauri = async () => {
|
||||
// We want to invoke our command to login via device auth.
|
||||
@ -21,8 +22,7 @@ const SignIn = () => {
|
||||
const token: string = await invoke('login', {
|
||||
host: VITE_KC_API_BASE_URL,
|
||||
})
|
||||
setToken(token)
|
||||
navigate(paths.INDEX)
|
||||
send({ type: 'tryLogin', token })
|
||||
} catch (error) {
|
||||
console.error('login button', error)
|
||||
}
|
||||
|
@ -114,20 +114,6 @@ interface DefaultDir {
|
||||
|
||||
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
|
||||
|
||||
// TODO: import real OpenAPI User type from schema
|
||||
export interface User {
|
||||
company?: string
|
||||
created_at: string
|
||||
email: string
|
||||
first_name?: string
|
||||
id: string
|
||||
image?: string
|
||||
last_name?: string
|
||||
name?: string
|
||||
phone?: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
editorView: EditorView | null
|
||||
setEditorView: (editorView: EditorView) => void
|
||||
@ -219,10 +205,6 @@ export interface StoreState {
|
||||
path: string
|
||||
}[]
|
||||
setHomeMenuItems: (items: { name: string; path: string }[]) => void
|
||||
token: string
|
||||
setToken: (token: string) => void
|
||||
user?: User
|
||||
setUser: (user: User | undefined) => void
|
||||
debugPanel: boolean
|
||||
setDebugPanel: (debugPanel: boolean) => void
|
||||
}
|
||||
@ -423,10 +405,6 @@ export const useStore = create<StoreState>()(
|
||||
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
||||
homeMenuItems: [],
|
||||
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
||||
token: '',
|
||||
setToken: (token) => set({ token }),
|
||||
user: undefined,
|
||||
setUser: (user) => set({ user }),
|
||||
debugPanel: false,
|
||||
setDebugPanel: (debugPanel) => set({ debugPanel }),
|
||||
}),
|
||||
@ -441,7 +419,6 @@ export const useStore = create<StoreState>()(
|
||||
'defaultProjectName',
|
||||
'defaultUnitSystem',
|
||||
'defaultBaseUnit',
|
||||
'token',
|
||||
'debugPanel',
|
||||
'onboardingStatus',
|
||||
'theme',
|
||||
|
17
yarn.lock
17
yarn.lock
@ -2506,6 +2506,14 @@
|
||||
loupe "^2.3.6"
|
||||
pretty-format "^29.5.0"
|
||||
|
||||
"@xstate/react@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8"
|
||||
integrity sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ==
|
||||
dependencies:
|
||||
use-isomorphic-layout-effect "^1.1.2"
|
||||
use-sync-external-store "^1.0.0"
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@ -5893,7 +5901,7 @@ use-composed-ref@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
|
||||
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
|
||||
|
||||
use-isomorphic-layout-effect@^1.1.1:
|
||||
use-isomorphic-layout-effect@^1.1.1, use-isomorphic-layout-effect@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
||||
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
||||
@ -5905,7 +5913,7 @@ use-latest@^1.2.1:
|
||||
dependencies:
|
||||
use-isomorphic-layout-effect "^1.1.1"
|
||||
|
||||
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
|
||||
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
@ -6112,6 +6120,11 @@ ws@^8.13.0:
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||
|
||||
xstate@^4.38.2:
|
||||
version "4.38.2"
|
||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
|
||||
integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==
|
||||
|
||||
yallist@^3.0.2:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||
|
Reference in New Issue
Block a user