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,7 @@ import { Toolbar } from '../Toolbar'
 | 
			
		||||
import UserSidebarMenu from './UserSidebarMenu'
 | 
			
		||||
import { ProjectWithEntryPointMetadata } from '../Router'
 | 
			
		||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
 | 
			
		||||
import { useAuthMachine } from '../hooks/useAuthMachine'
 | 
			
		||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
 | 
			
		||||
 | 
			
		||||
interface AppHeaderProps extends React.PropsWithChildren {
 | 
			
		||||
  showToolbar?: boolean
 | 
			
		||||
@ -18,7 +18,11 @@ export const AppHeader = ({
 | 
			
		||||
  className = '',
 | 
			
		||||
  enableMenu = false,
 | 
			
		||||
}: AppHeaderProps) => {
 | 
			
		||||
  const [user] = useAuthMachine((s) => s?.context?.user)
 | 
			
		||||
  const {
 | 
			
		||||
    auth: {
 | 
			
		||||
      context: { user },
 | 
			
		||||
    },
 | 
			
		||||
  } = useGlobalStateContext()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <header
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ import {
 | 
			
		||||
  Fragment,
 | 
			
		||||
  SetStateAction,
 | 
			
		||||
  createContext,
 | 
			
		||||
  useContext,
 | 
			
		||||
  useState,
 | 
			
		||||
} from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
@ -12,6 +11,7 @@ import { ActionIcon } from './ActionIcon'
 | 
			
		||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import Fuse from 'fuse.js'
 | 
			
		||||
import { Command, SubCommand } from '../lib/commands'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
 | 
			
		||||
export type SortedCommand = {
 | 
			
		||||
  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 { commands, commandBarOpen, setCommandBarOpen } =
 | 
			
		||||
    useContext(CommandsContext)
 | 
			
		||||
  const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  useHotkeys('meta+k', () => {
 | 
			
		||||
    if (commands.length === 0) return
 | 
			
		||||
    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 { BrowserRouter } from 'react-router-dom'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import { GlobalStateProvider } from '../hooks/useAuthMachine'
 | 
			
		||||
import { GlobalStateProvider } from './GlobalStateProvider'
 | 
			
		||||
import CommandBarProvider from './CommandBar'
 | 
			
		||||
 | 
			
		||||
type User = Models['User_type']
 | 
			
		||||
 | 
			
		||||
@ -94,7 +95,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
 | 
			
		||||
  // wrap in router and xState context
 | 
			
		||||
  return (
 | 
			
		||||
    <BrowserRouter>
 | 
			
		||||
      <GlobalStateProvider>{children}</GlobalStateProvider>
 | 
			
		||||
      <CommandBarProvider>
 | 
			
		||||
        <GlobalStateProvider>{children}</GlobalStateProvider>
 | 
			
		||||
      </CommandBarProvider>
 | 
			
		||||
    </BrowserRouter>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,8 @@ 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'
 | 
			
		||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
 | 
			
		||||
 | 
			
		||||
type User = Models['User_type']
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,9 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
 | 
			
		||||
  const displayedName = getDisplayName(user)
 | 
			
		||||
  const [imageLoadFailed, setImageLoadFailed] = useState(false)
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const [_, send] = useAuthMachine()
 | 
			
		||||
  const {
 | 
			
		||||
    auth: { send },
 | 
			
		||||
  } = useGlobalStateContext()
 | 
			
		||||
 | 
			
		||||
  // Fallback logic for displaying user's "name":
 | 
			
		||||
  // 1. user.name
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user