[Refactor] decouple settingsMachine from React (#5142)
* Remove unnecessary console.log
* Create a global appMachine
* Strip authMachine of side-effects
* Replace react-bound authMachine use with XState actor use
* Fix import goof
* Register auth commands directly!
* Don't provide anything to settingsMachine from React
* Remove unecessary async
* Make it possible to load project settings via a sent event, without React
* Make settingsMachine ready to be an actor
* Remove settingsLoader use
* Replace all useSettingsAuthContext use with direct actor use
* Add logic to clear project settings, fmt
* fmt
* Clear and load project settings from routeLoaders, but using actor
* Remove useRefreshSettings
* Restore use of useToken() that wasn't working for some reason
* Migrate useFileSystemWatcher use to RouteProvider
* Surface wasm_bindgen unavailable error to console
* Remove unnecessary use of Jest settings wrappers
* Replace dynamic import with actor.getSnapshot
* Migrate system theme and theme color watching from useEffects to actors/actions
* Migrate cursor color effect
* Remove unused code that is now in RouteProvider
* Migrate route commands registration further down for now, out of SettingsAuthProvider
* Migrate settings command registration out of SettingsAuthProvider.tsx
* Delete SettingsAuthProvider.tsx!
* Remove unused settingsLoader!
* fmt and remove comments
* Use actor for routeLoader
* Fix project read error due to uninitialized WASM
* Fix user settings load error due to uninitialized WASM
* Move settingsActor into appActor as a spawned child
* Trying to fix unit tests
* Remove unused imports and demo window attachments
* fmt
* Fix testing issues caused by circular dependency
* Add `setThemeColor` to a few actions list it was missing from
* fmt
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Fix "Execute AST" action in browser, where currentProject is `undefined`
* Update commands list when currentProject changes
* Fix `clearProjectSettings`, which was passing along non-settings context
* Fix onboarding test that actually needed the onboarding initially dismissed
* Add scrollIntoView to make this test more reliable
* @lf94's feedback I missed
I got distracted by a million other things last week
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)"
This reverts commit 129226c6ef
.
* fmt
* revert bad snapshot
* Fix up camera movement test locator
* Fix test that was flipping the user settings without waiting
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,24 +1,22 @@
|
||||
import { Switch } from '@headlessui/react'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function CameraProjectionToggle() {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
const isCameraProjectionPerspective =
|
||||
settings.context.modeling.cameraProjection.current === 'perspective'
|
||||
settings.modeling.cameraProjection.current === 'perspective'
|
||||
const [checked, setChecked] = useState(isCameraProjectionPerspective)
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(
|
||||
settings.context.modeling.cameraProjection.current === 'perspective'
|
||||
)
|
||||
}, [settings.context.modeling.cameraProjection.current])
|
||||
setChecked(settings.modeling.cameraProjection.current === 'perspective')
|
||||
}, [settings.modeling.cameraProjection.current])
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={(newValue) => {
|
||||
settings.send({
|
||||
settingsActor.send({
|
||||
type: 'set.modeling.cameraProjection',
|
||||
data: {
|
||||
level: 'user',
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@codemirror/autocomplete'
|
||||
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
||||
import { getSystemTheme } from 'lib/theme'
|
||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||
@ -20,6 +19,7 @@ import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
const machineContextSelector = (snapshot?: {
|
||||
@ -42,7 +42,7 @@ function CommandBarKclInput({
|
||||
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||
arg.name
|
||||
] as KclCommandValue | undefined
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
const argMachineContext = useSelector(
|
||||
arg.machineActor,
|
||||
machineContextSelector
|
||||
@ -117,9 +117,9 @@ function CommandBarKclInput({
|
||||
: defaultValue.length,
|
||||
},
|
||||
theme:
|
||||
settings.context.app.theme.current === 'system'
|
||||
settings.app.theme.current === 'system'
|
||||
? getSystemTheme()
|
||||
: settings.context.app.theme.current,
|
||||
: settings.app.theme.current,
|
||||
extensions: [
|
||||
varMentionsExtension,
|
||||
EditorView.updateListener.of((vu: ViewUpdate) => {
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
|
||||
const DownloadAppBanner = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
const [isBannerDismissed, setIsBannerDismissed] = useState(
|
||||
settings.context.app.dismissWebBanner.current || hasCreateFileParam
|
||||
settings.app.dismissWebBanner.current
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||
import { type IndexLoaderData } from 'lib/types'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { BROWSER_PATH, PATHS } from 'lib/paths'
|
||||
import React, { createContext, useEffect, useMemo } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
@ -27,9 +27,10 @@ import {
|
||||
getKclSamplesManifest,
|
||||
KclSamplesManifestItem,
|
||||
} from 'lib/getKclSamplesManifest'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
@ -48,14 +49,51 @@ export const FileMachineProvider = ({
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const location = useLocation()
|
||||
const token = useToken()
|
||||
const settings = useSettings()
|
||||
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { project, file } = projectData
|
||||
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
||||
[]
|
||||
)
|
||||
|
||||
// Due to the route provider, i've moved this to the FileMachineProvider instead of CommandBarProvider
|
||||
// This will register the commands to route to Telemetry, Home, and Settings.
|
||||
useEffect(() => {
|
||||
const filePath =
|
||||
PATHS.FILE + '/' + encodeURIComponent(file?.path || BROWSER_PATH)
|
||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||
createRouteCommands(navigate, location, filePath)
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteHomeCommand,
|
||||
RouteSettingsCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
if (location.pathname === PATHS.HOME) {
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||
})
|
||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteSettingsCommand,
|
||||
RouteHomeCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [location])
|
||||
|
||||
useEffect(() => {
|
||||
markOnce('code/didLoadFile')
|
||||
async function fetchKclSamples() {
|
||||
@ -323,7 +361,7 @@ export const FileMachineProvider = ({
|
||||
authToken: token ?? '',
|
||||
projectData,
|
||||
settings: {
|
||||
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
|
||||
defaultUnit: settings.modeling.defaultUnit.current ?? 'mm',
|
||||
},
|
||||
specialPropsForSampleCommand: {
|
||||
onSubmit: async (data) => {
|
||||
@ -345,7 +383,7 @@ export const FileMachineProvider = ({
|
||||
// Either way, we want to overwrite the defaultUnit project setting
|
||||
// with the sample's setting.
|
||||
if (data.sampleUnits) {
|
||||
settings.send({
|
||||
settingsActor.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import Tooltip from './Tooltip'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
@ -9,6 +8,7 @@ import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { settingsActor } from 'machines/appMachine'
|
||||
|
||||
const HelpMenuDivider = () => (
|
||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
||||
@ -20,7 +20,6 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const isInProject = location.pathname.includes(PATHS.FILE)
|
||||
const navigate = useNavigate()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
@ -106,7 +105,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
<HelpMenuItem
|
||||
as="button"
|
||||
onClick={() => {
|
||||
settings.send({
|
||||
settingsActor.send({
|
||||
type: 'set.app.onboardingStatus',
|
||||
data: {
|
||||
value: '',
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
import { TEST, VITE_KC_API_BASE_URL } from 'env'
|
||||
import { kcl } from 'editor/plugins/lsp/kcl/language'
|
||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Extension } from '@codemirror/state'
|
||||
import { LanguageSupport } from '@codemirror/language'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
@ -22,7 +22,6 @@ import {
|
||||
modelingMachineDefaultContext,
|
||||
} from 'machines/modelingMachine'
|
||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import {
|
||||
isCursorInSketchCommandRange,
|
||||
updateSketchDetailsNodePaths,
|
||||
@ -110,6 +109,7 @@ import { kclEditorActor } from 'machines/kclEditorMachine'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -131,19 +131,15 @@ export const ModelingMachineProvider = ({
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const {
|
||||
settings: {
|
||||
context: {
|
||||
app: { theme, enableSSAO, allowOrbitInSketchMode },
|
||||
modeling: {
|
||||
defaultUnit,
|
||||
cameraProjection,
|
||||
highlightEdges,
|
||||
showScaleGrid,
|
||||
cameraOrbit,
|
||||
},
|
||||
},
|
||||
app: { theme, enableSSAO, allowOrbitInSketchMode },
|
||||
modeling: {
|
||||
defaultUnit,
|
||||
cameraProjection,
|
||||
highlightEdges,
|
||||
showScaleGrid,
|
||||
cameraOrbit,
|
||||
},
|
||||
} = useSettingsAuthContext()
|
||||
} = useSettings()
|
||||
const previousAllowOrbitInSketchMode = useRef(allowOrbitInSketchMode.current)
|
||||
const navigate = useNavigate()
|
||||
const { context, send: fileMachineSend } = useFileContext()
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { ReactNode } from 'react'
|
||||
import styles from './ModelingPane.module.css'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
|
||||
export interface ModelingPaneProps {
|
||||
id: string
|
||||
@ -68,8 +68,8 @@ export const ModelingPane = ({
|
||||
title,
|
||||
...props
|
||||
}: ModelingPaneProps) => {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const onboardingStatus = settings.context.app.onboardingStatus
|
||||
const settings = useSettings()
|
||||
const onboardingStatus = settings.app.onboardingStatus
|
||||
const pointerEventsCssClass =
|
||||
onboardingStatus.current === onboardingPaths.CAMERA
|
||||
? 'pointer-events-none '
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { TEST } from 'env'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||
@ -51,6 +50,7 @@ import {
|
||||
} from 'machines/kclEditorMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { modelingMachineEvent } from 'editor/manager'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
@ -63,9 +63,7 @@ export const editorShortcutMeta = {
|
||||
}
|
||||
|
||||
export const KclEditorPane = () => {
|
||||
const {
|
||||
settings: { context },
|
||||
} = useSettingsAuthContext()
|
||||
const context = useSettings()
|
||||
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
|
||||
const editorIsMounted = useSelector(kclEditorActor, editorIsMountedSelector)
|
||||
const theme =
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Resizable } from 're-resizable'
|
||||
import {
|
||||
MouseEventHandler,
|
||||
@ -21,6 +20,7 @@ import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
@ -38,23 +38,23 @@ function getPlatformString(): 'web' | 'desktop' {
|
||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
const kclContext = useKclContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const onboardingStatus = settings.context.app.onboardingStatus
|
||||
const settings = useSettings()
|
||||
const onboardingStatus = settings.app.onboardingStatus
|
||||
const { send, context } = useModelingContext()
|
||||
const pointerEventsCssClass =
|
||||
onboardingStatus.current === onboardingPaths.CAMERA ||
|
||||
context.store?.openPanes.length === 0
|
||||
? 'pointer-events-none '
|
||||
: 'pointer-events-auto '
|
||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||
const showDebugPanel = settings.modeling.showDebugPanel
|
||||
|
||||
const paneCallbackProps = useMemo(
|
||||
() => ({
|
||||
kclContext,
|
||||
settings: settings.context,
|
||||
settings,
|
||||
platform: getPlatformString(),
|
||||
}),
|
||||
[kclContext.diagnostics, settings.context]
|
||||
[kclContext.diagnostics, settings]
|
||||
)
|
||||
|
||||
const sidebarActions: SidebarAction[] = [
|
||||
@ -144,7 +144,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [settings.context])
|
||||
}, [settings.modeling.showDebugPanel])
|
||||
|
||||
const togglePane = useCallback(
|
||||
(newPane: SidebarType) => {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
import {
|
||||
NETWORK_HEALTH_TEXT,
|
||||
NetworkHealthIndicator,
|
||||
@ -9,11 +8,7 @@ import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||
</BrowserRouter>
|
||||
)
|
||||
return <BrowserRouter>{children}</BrowserRouter>
|
||||
}
|
||||
|
||||
// Our Playwright tests for this are much more comprehensive.
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
const now = new Date()
|
||||
@ -32,9 +31,7 @@ describe('ProjectSidebarMenu tests', () => {
|
||||
test('Disables popover menu by default', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SettingsAuthProviderJest>
|
||||
<ProjectSidebarMenu project={projectWellFormed} />
|
||||
</SettingsAuthProviderJest>
|
||||
<ProjectSidebarMenu project={projectWellFormed} />
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
|
@ -18,7 +18,6 @@ import { SnapshotFrom } from 'xstate'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { copyFileShareLink } from 'lib/links'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
@ -103,7 +102,6 @@ function ProjectMenuPopover({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
useSettingsAuthContext()
|
||||
const token = useToken()
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
const commands = useSelector(commandBarActor, commandsSelector)
|
||||
|
@ -20,11 +20,11 @@ import {
|
||||
getUniqueProjectName,
|
||||
getNextFileName,
|
||||
} from 'lib/desktopFS'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
import {
|
||||
CREATE_FILE_URL_PARAM,
|
||||
FILE_EXT,
|
||||
@ -77,9 +77,7 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
||||
searchParams.delete('units')
|
||||
setSearchParams(searchParams)
|
||||
}, [searchParams, setSearchParams])
|
||||
const {
|
||||
settings: { context: settings },
|
||||
} = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
|
||||
const [state, send, actor] = useMachine(
|
||||
projectsMachine.provide({
|
||||
@ -183,9 +181,7 @@ const ProjectsContextDesktop = ({
|
||||
setSearchParams(searchParams)
|
||||
}, [searchParams, setSearchParams])
|
||||
const { onProjectOpen } = useLspContext()
|
||||
const {
|
||||
settings: { context: settings },
|
||||
} = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
|
||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
||||
|
@ -5,7 +5,6 @@ import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import React, { useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import Tooltip from './Tooltip'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
@ -1,17 +1,36 @@
|
||||
import { useEffect, useState, createContext, ReactNode } from 'react'
|
||||
import { useNavigation, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
useNavigation,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useRouteLoaderData,
|
||||
} from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { useAuthNavigation } from 'hooks/useAuthNavigation'
|
||||
import { useAuthState } from 'machines/appMachine'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { getAppSettingsFilePath } from 'lib/desktop'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { trap } from 'lib/trap'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { loadAndValidateSettings } from 'lib/settings/settingsUtils'
|
||||
import { settingsActor } from 'machines/appMachine'
|
||||
|
||||
export const RouteProviderContext = createContext({})
|
||||
|
||||
export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
useAuthNavigation()
|
||||
const loadedProject = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const [first, setFirstState] = useState(true)
|
||||
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const navigation = useNavigation()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
const authState = useAuthState()
|
||||
useEffect(() => {
|
||||
// On initialization, the react-router-dom does not send a 'loading' state event.
|
||||
// it sends an idle event first.
|
||||
@ -28,6 +47,41 @@ export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
setFirstState(false)
|
||||
}, [navigation])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop()) return
|
||||
getAppSettingsFilePath().then(setSettingsPath).catch(trap)
|
||||
}, [])
|
||||
|
||||
useFileSystemWatcher(
|
||||
async (eventType: string) => {
|
||||
// If there is a projectPath but it no longer exists it means
|
||||
// it was exterally removed. If we let the code past this condition
|
||||
// execute it will recreate the directory due to code in
|
||||
// loadAndValidateSettings trying to recreate files. I do not
|
||||
// wish to change the behavior in case anything else uses it.
|
||||
// Go home.
|
||||
if (loadedProject?.project?.path) {
|
||||
if (!window.electron.exists(loadedProject?.project?.path)) {
|
||||
navigate(PATHS.HOME)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Only reload if there are changes. Ignore everything else.
|
||||
if (eventType !== 'change') return
|
||||
|
||||
const data = await loadAndValidateSettings(loadedProject?.project?.path)
|
||||
settingsActor.send({
|
||||
type: 'Set all settings',
|
||||
settings: data.settings,
|
||||
doNotPersist: true,
|
||||
})
|
||||
},
|
||||
[settingsPath, loadedProject?.project?.path].filter(
|
||||
(x: string | undefined) => x !== undefined
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<RouteProviderContext.Provider value={{}}>
|
||||
{children}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import decamelize from 'decamelize'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Setting } from 'lib/settings/initialSettings'
|
||||
import { SetEventTypes, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||
import {
|
||||
@ -25,6 +24,8 @@ import { useLspContext } from 'components/LspProvider'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
|
||||
interface AllSettingsFieldsProps {
|
||||
searchParamTab: SettingsLevel
|
||||
@ -40,9 +41,7 @@ export const AllSettingsFields = forwardRef(
|
||||
const navigate = useNavigate()
|
||||
const { onProjectOpen } = useLspContext()
|
||||
const dotDotSlash = useDotDotSlash()
|
||||
const {
|
||||
settings: { send, context, state },
|
||||
} = useSettingsAuthContext()
|
||||
const context = useSettings()
|
||||
|
||||
const projectPath = useMemo(() => {
|
||||
const filteredPathname = location.pathname
|
||||
@ -62,7 +61,7 @@ export const AllSettingsFields = forwardRef(
|
||||
}, [location.pathname])
|
||||
|
||||
function restartOnboarding() {
|
||||
send({
|
||||
settingsActor.send({
|
||||
type: `set.app.onboardingStatus`,
|
||||
data: { level: 'user', value: '' },
|
||||
})
|
||||
@ -72,11 +71,14 @@ export const AllSettingsFields = forwardRef(
|
||||
* A "listener" for the XState to return to "idle" state
|
||||
* when the user resets the onboarding, using the callback above
|
||||
*/
|
||||
const isSettingsMachineIdle = useSelector(settingsActor, (s) =>
|
||||
s.matches('idle')
|
||||
)
|
||||
useEffect(() => {
|
||||
async function navigateToOnboardingStart() {
|
||||
if (
|
||||
state.context.app.onboardingStatus.user === '' &&
|
||||
state.matches('idle')
|
||||
context.app.onboardingStatus.current === '' &&
|
||||
isSettingsMachineIdle
|
||||
) {
|
||||
if (isFileSettings) {
|
||||
// If we're in a project, first navigate to the onboarding start here
|
||||
@ -91,7 +93,12 @@ export const AllSettingsFields = forwardRef(
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
navigateToOnboardingStart()
|
||||
}, [isFileSettings, navigate, state])
|
||||
}, [
|
||||
isFileSettings,
|
||||
navigate,
|
||||
isSettingsMachineIdle,
|
||||
context.app.onboardingStatus.current,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="relative overflow-y-auto">
|
||||
@ -142,7 +149,7 @@ export const AllSettingsFields = forwardRef(
|
||||
}
|
||||
parentLevel={setting.getParentLevel(searchParamTab)}
|
||||
onFallback={() =>
|
||||
send({
|
||||
settingsActor.send({
|
||||
type: `set.${category}.${settingName}`,
|
||||
data: {
|
||||
level: searchParamTab,
|
||||
@ -218,7 +225,7 @@ export const AllSettingsFields = forwardRef(
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
send({
|
||||
settingsActor.send({
|
||||
type: 'Reset settings',
|
||||
level: searchParamTab,
|
||||
})
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Toggle } from 'components/Toggle/Toggle'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Setting } from 'lib/settings/initialSettings'
|
||||
import {
|
||||
SetEventTypes,
|
||||
@ -7,6 +6,7 @@ import {
|
||||
WildcardSetEvent,
|
||||
} from 'lib/settings/settingsTypes'
|
||||
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
||||
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||
import { useMemo } from 'react'
|
||||
import { EventFrom } from 'xstate'
|
||||
|
||||
@ -25,9 +25,8 @@ export function SettingsFieldInput({
|
||||
settingsLevel,
|
||||
setting,
|
||||
}: SettingsFieldInputProps) {
|
||||
const {
|
||||
settings: { context, send },
|
||||
} = useSettingsAuthContext()
|
||||
const context = useSettings()
|
||||
const send = settingsActor.send
|
||||
const options = useMemo(() => {
|
||||
return setting.commandConfig &&
|
||||
'options' in setting.commandConfig &&
|
||||
|
@ -2,10 +2,10 @@ import { Combobox } from '@headlessui/react'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import decamelize from 'decamelize'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { interactionMap } from 'lib/settings/initialKeybindings'
|
||||
import { Setting } from 'lib/settings/initialSettings'
|
||||
import { SettingsLevel } from 'lib/settings/settingsTypes'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@ -32,23 +32,22 @@ export function SettingsSearchBar() {
|
||||
)
|
||||
const navigate = useNavigate()
|
||||
const [query, setQuery] = useState('')
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
const settingsAsSearchable: SettingsSearchItem[] = useMemo(
|
||||
() => [
|
||||
...Object.entries(settings.state.context).flatMap(
|
||||
([category, categorySettings]) =>
|
||||
Object.entries(categorySettings).flatMap(([settingName, setting]) => {
|
||||
const s = setting as Setting
|
||||
return (['project', 'user'] satisfies SettingsLevel[])
|
||||
.filter((l) => s.hideOnLevel !== l)
|
||||
.map((l) => ({
|
||||
category: decamelize(category, { separator: ' ' }),
|
||||
name: settingName,
|
||||
description: s.description ?? '',
|
||||
displayName: decamelize(settingName, { separator: ' ' }),
|
||||
level: l as ExtendedSettingsLevel,
|
||||
}))
|
||||
})
|
||||
...Object.entries(settings).flatMap(([category, categorySettings]) =>
|
||||
Object.entries(categorySettings).flatMap(([settingName, setting]) => {
|
||||
const s = setting
|
||||
return (['project', 'user'] satisfies SettingsLevel[])
|
||||
.filter((l) => s.hideOnLevel !== l)
|
||||
.map((l) => ({
|
||||
category: decamelize(category, { separator: ' ' }),
|
||||
name: settingName,
|
||||
description: s.description ?? '',
|
||||
displayName: decamelize(settingName, { separator: ' ' }),
|
||||
level: l,
|
||||
}))
|
||||
})
|
||||
),
|
||||
...Object.entries(interactionMap).flatMap(
|
||||
([category, categoryKeybindings]) =>
|
||||
@ -61,7 +60,7 @@ export function SettingsSearchBar() {
|
||||
}))
|
||||
),
|
||||
],
|
||||
[settings.state.context]
|
||||
[settings]
|
||||
)
|
||||
const [searchResults, setSearchResults] = useState(settingsAsSearchable)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import decamelize from 'decamelize'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Setting } from 'lib/settings/initialSettings'
|
||||
import { SettingsLevel } from 'lib/settings/settingsTypes'
|
||||
import { shouldHideSetting } from 'lib/settings/settingsUtils'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
|
||||
interface SettingsSectionsListProps {
|
||||
searchParamTab: SettingsLevel
|
||||
@ -13,9 +13,7 @@ export function SettingsSectionsList({
|
||||
searchParamTab,
|
||||
scrollRef,
|
||||
}: SettingsSectionsListProps) {
|
||||
const {
|
||||
settings: { context },
|
||||
} = useSettingsAuthContext()
|
||||
const context = useSettings()
|
||||
return (
|
||||
<div className="flex w-32 flex-col gap-3 pr-2 py-1 border-0 border-r border-r-chalkboard-20 dark:border-r-chalkboard-90">
|
||||
{Object.entries(context)
|
||||
|
@ -1,383 +0,0 @@
|
||||
import { trap } from 'lib/trap'
|
||||
import { useMachine, useSelector } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||
import React, { createContext, useEffect, useState } from 'react'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
darkModeMatcher,
|
||||
getOppositeTheme,
|
||||
setThemeClass,
|
||||
Themes,
|
||||
} from 'lib/theme'
|
||||
import decamelize from 'decamelize'
|
||||
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||
import {
|
||||
kclManager,
|
||||
sceneInfra,
|
||||
engineCommandManager,
|
||||
sceneEntitiesManager,
|
||||
} from 'lib/singletons'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { settings } from 'lib/settings/initialSettings'
|
||||
import {
|
||||
createSettingsCommand,
|
||||
settingsWithCommandConfigs,
|
||||
} from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||
import { Command } from 'lib/commandTypes'
|
||||
import { BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import {
|
||||
saveSettings,
|
||||
loadAndValidateSettings,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { getAppSettingsFilePath } from 'lib/desktop'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
|
||||
type SettingsAuthContextType = {
|
||||
settings: MachineContext<typeof settingsMachine>
|
||||
}
|
||||
|
||||
/**
|
||||
* This variable is used to store the last snapshot of the settings context
|
||||
* for use outside of React, such as in `wasm.ts`. It is updated every time
|
||||
* the settings machine changes with `useSelector`.
|
||||
* TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
|
||||
*/
|
||||
export let lastSettingsContextSnapshot:
|
||||
| ContextFrom<typeof settingsMachine>
|
||||
| undefined
|
||||
|
||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||
|
||||
export const SettingsAuthProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const loadedSettings = useRouteLoaderData(PATHS.INDEX) as typeof settings
|
||||
const loadedProject = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
return (
|
||||
<SettingsAuthProviderBase
|
||||
loadedSettings={loadedSettings}
|
||||
loadedProject={loadedProject}
|
||||
>
|
||||
{children}
|
||||
</SettingsAuthProviderBase>
|
||||
)
|
||||
}
|
||||
|
||||
// For use in jest tests we don't want to use the loader data
|
||||
// and mock the whole Router
|
||||
export const SettingsAuthProviderJest = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const loadedSettings = settings
|
||||
return (
|
||||
<SettingsAuthProviderBase loadedSettings={loadedSettings}>
|
||||
{children}
|
||||
</SettingsAuthProviderBase>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingsAuthProviderBase = ({
|
||||
children,
|
||||
loadedSettings,
|
||||
loadedProject,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
loadedSettings: typeof settings
|
||||
loadedProject?: IndexLoaderData
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||
settingsMachine.provide({
|
||||
actions: {
|
||||
//TODO: batch all these and if that's difficult to do from tsx,
|
||||
// make it easy to do
|
||||
|
||||
setClientSideSceneUnits: ({ context, event }) => {
|
||||
const newBaseUnit =
|
||||
event.type === 'set.modeling.defaultUnit'
|
||||
? (event.data.value as BaseUnit)
|
||||
: context.modeling.defaultUnit.current
|
||||
sceneInfra.baseUnit = newBaseUnit
|
||||
},
|
||||
setEngineTheme: ({ context }) => {
|
||||
engineCommandManager
|
||||
.setTheme(context.app.theme.current)
|
||||
.catch(reportRejection)
|
||||
},
|
||||
setClientTheme: ({ context }) => {
|
||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||
sceneInfra.theme = opposingTheme
|
||||
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
||||
},
|
||||
setAllowOrbitInSketchMode: ({ context }) => {
|
||||
sceneInfra.camControls._setting_allowOrbitInSketchMode =
|
||||
context.app.allowOrbitInSketchMode.current
|
||||
// ModelingMachineProvider will do a use effect to trigger the camera engine sync
|
||||
},
|
||||
toastSuccess: ({ event }) => {
|
||||
if (!('data' in event)) return
|
||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||
keyof typeof settings,
|
||||
string
|
||||
]
|
||||
const truncatedNewValue = event.data.value?.toString().slice(0, 28)
|
||||
const message =
|
||||
`Set ${decamelize(eventParts[1], { separator: ' ' })}` +
|
||||
(truncatedNewValue
|
||||
? ` to "${truncatedNewValue}${
|
||||
truncatedNewValue.length === 28 ? '...' : ''
|
||||
}"${
|
||||
event.data.level === 'project'
|
||||
? ' for this project'
|
||||
: ' as a user default'
|
||||
}`
|
||||
: '')
|
||||
toast.success(message, {
|
||||
duration: message.split(' ').length * 100 + 1500,
|
||||
id: `${event.type}.success`,
|
||||
})
|
||||
},
|
||||
'Execute AST': ({ context, event }) => {
|
||||
try {
|
||||
const relevantSetting = (s: typeof settings) => {
|
||||
return (
|
||||
s.modeling?.defaultUnit?.current !==
|
||||
context.modeling.defaultUnit.current ||
|
||||
s.modeling.showScaleGrid.current !==
|
||||
context.modeling.showScaleGrid.current ||
|
||||
s.modeling?.highlightEdges.current !==
|
||||
context.modeling.highlightEdges.current
|
||||
)
|
||||
}
|
||||
|
||||
const allSettingsIncludesUnitChange =
|
||||
event.type === 'Set all settings' &&
|
||||
relevantSetting(event.settings)
|
||||
const resetSettingsIncludesUnitChange =
|
||||
event.type === 'Reset settings' && relevantSetting(settings)
|
||||
|
||||
if (
|
||||
event.type === 'set.modeling.defaultUnit' ||
|
||||
event.type === 'set.modeling.showScaleGrid' ||
|
||||
event.type === 'set.modeling.highlightEdges' ||
|
||||
allSettingsIncludesUnitChange ||
|
||||
resetSettingsIncludesUnitChange
|
||||
) {
|
||||
// Unit changes requires a re-exec of code
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
kclManager.executeCode(true)
|
||||
} else {
|
||||
// For any future logging we'd like to do
|
||||
// console.log(
|
||||
// 'Not re-executing AST because the settings change did not affect the code interpretation'
|
||||
// )
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error executing AST after settings change', e)
|
||||
}
|
||||
},
|
||||
async persistSettings({ context, event }) {
|
||||
// Without this, when a user changes the file, it'd
|
||||
// create a detection loop with the file-system watcher.
|
||||
if (event.doNotPersist) return
|
||||
|
||||
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
|
||||
return saveSettings(context, loadedProject?.project?.path)
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ input: loadedSettings }
|
||||
)
|
||||
// Any time the actor changes, update the settings state for external use
|
||||
useSelector(settingsActor, (s) => {
|
||||
lastSettingsContextSnapshot = s.context
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop()) return
|
||||
getAppSettingsFilePath().then(setSettingsPath).catch(trap)
|
||||
}, [])
|
||||
|
||||
useFileSystemWatcher(
|
||||
async (eventType: string) => {
|
||||
// If there is a projectPath but it no longer exists it means
|
||||
// it was exterally removed. If we let the code past this condition
|
||||
// execute it will recreate the directory due to code in
|
||||
// loadAndValidateSettings trying to recreate files. I do not
|
||||
// wish to change the behavior in case anything else uses it.
|
||||
// Go home.
|
||||
if (loadedProject?.project?.path) {
|
||||
if (!window.electron.exists(loadedProject?.project?.path)) {
|
||||
navigate(PATHS.HOME)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Only reload if there are changes. Ignore everything else.
|
||||
if (eventType !== 'change') return
|
||||
|
||||
const data = await loadAndValidateSettings(loadedProject?.project?.path)
|
||||
settingsSend({
|
||||
type: 'Set all settings',
|
||||
settings: data.settings,
|
||||
doNotPersist: true,
|
||||
})
|
||||
},
|
||||
[settingsPath, loadedProject?.project?.path].filter(
|
||||
(x: string | undefined) => x !== undefined
|
||||
)
|
||||
)
|
||||
|
||||
// Add settings commands to the command bar
|
||||
// They're treated slightly differently than other commands
|
||||
// Because their state machine doesn't have a meaningful .nextEvents,
|
||||
// and they are configured statically in initialiSettings
|
||||
useEffect(() => {
|
||||
// If the user wants to hide the settings commands
|
||||
//from the command bar don't add them.
|
||||
if (settingsState.context.commandBar.includeSettings.current === false)
|
||||
return
|
||||
|
||||
const commands = settingsWithCommandConfigs(settingsState.context)
|
||||
.map((type) =>
|
||||
createSettingsCommand({
|
||||
type,
|
||||
send: settingsSend,
|
||||
context: settingsState.context,
|
||||
actor: settingsActor,
|
||||
isProjectAvailable: loadedProject !== undefined,
|
||||
})
|
||||
)
|
||||
.filter((c) => c !== null) as Command[]
|
||||
|
||||
commandBarActor.send({ type: 'Add commands', data: { commands: commands } })
|
||||
|
||||
return () => {
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: { commands },
|
||||
})
|
||||
}
|
||||
}, [
|
||||
settingsState,
|
||||
settingsSend,
|
||||
settingsActor,
|
||||
commandBarActor.send,
|
||||
settingsWithCommandConfigs,
|
||||
])
|
||||
|
||||
// Due to the route provider, i've moved this to the SettingsAuthProvider instead of CommandBarProvider
|
||||
// This will register the commands to route to Telemetry, Home, and Settings.
|
||||
useEffect(() => {
|
||||
const filePath =
|
||||
PATHS.FILE +
|
||||
'/' +
|
||||
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||
createRouteCommands(navigate, location, filePath)
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteHomeCommand,
|
||||
RouteSettingsCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
if (location.pathname === PATHS.HOME) {
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||
})
|
||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteSettingsCommand,
|
||||
RouteHomeCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [location])
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
// This is only done if the theme setting is set to 'system'.
|
||||
// It can't be done in XState (in an invoked callback, for example)
|
||||
// because there doesn't seem to be a good way to listen to
|
||||
// events outside of the machine that also depend on the machine's context
|
||||
useEffect(() => {
|
||||
const listener = (e: MediaQueryListEvent) => {
|
||||
if (settingsState.context.app.theme.current !== 'system') return
|
||||
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
|
||||
}
|
||||
|
||||
darkModeMatcher?.addEventListener('change', listener)
|
||||
return () => darkModeMatcher?.removeEventListener('change', listener)
|
||||
}, [settingsState.context])
|
||||
|
||||
/**
|
||||
* Update the --primary-hue CSS variable
|
||||
* to match the setting app.themeColor.current
|
||||
*/
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--primary-hue`,
|
||||
settingsState.context.app.themeColor.current
|
||||
)
|
||||
}, [settingsState.context.app.themeColor.current])
|
||||
|
||||
/**
|
||||
* Update the --cursor-color CSS variable
|
||||
* based on the setting textEditor.blinkingCursor.current
|
||||
*/
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--cursor-color`,
|
||||
settingsState.context.textEditor.blinkingCursor.current
|
||||
? 'auto'
|
||||
: 'transparent'
|
||||
)
|
||||
}, [settingsState.context.textEditor.blinkingCursor.current])
|
||||
|
||||
return (
|
||||
<SettingsAuthContext.Provider
|
||||
value={{
|
||||
settings: {
|
||||
state: settingsState,
|
||||
context: settingsState.context,
|
||||
send: settingsSend,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingsAuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsAuthProvider
|
@ -1,6 +1,5 @@
|
||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||
import Loading from './Loading'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
@ -20,8 +19,8 @@ import { IndexLoaderData } from 'lib/types'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
|
||||
enum StreamState {
|
||||
Playing = 'playing',
|
||||
@ -34,7 +33,7 @@ export const Stream = () => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
const { state, send } = useModelingContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const { mediaStream } = useAppStream()
|
||||
@ -42,7 +41,7 @@ export const Stream = () => {
|
||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
||||
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
|
||||
const IDLE = settings.context.app.streamIdleMode.current
|
||||
const IDLE = settings.app.streamIdleMode.current
|
||||
|
||||
const isNetworkOkay =
|
||||
overallState === NetworkHealthState.Ok ||
|
||||
@ -336,7 +335,7 @@ export const Stream = () => {
|
||||
id="video-stream"
|
||||
/>
|
||||
<ClientSideScene
|
||||
cameraControls={settings.context.modeling.mouseControls.current}
|
||||
cameraControls={settings.modeling.mouseControls.current}
|
||||
/>
|
||||
{(streamState === StreamState.Paused ||
|
||||
streamState === StreamState.Resuming) && (
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||
import { changeKclSettings, unitLengthToUnitLen } from 'lang/wasm'
|
||||
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
@ -8,24 +8,25 @@ import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export function UnitsMenu() {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
const [hasPerFileLengthUnit, setHasPerFileLengthUnit] = useState(
|
||||
Boolean(kclManager.fileSettings.defaultLengthUnit)
|
||||
)
|
||||
const [lengthSetting, setLengthSetting] = useState(
|
||||
kclManager.fileSettings.defaultLengthUnit ||
|
||||
settings.context.modeling.defaultUnit.current
|
||||
settings.modeling.defaultUnit.current
|
||||
)
|
||||
useEffect(() => {
|
||||
setHasPerFileLengthUnit(Boolean(kclManager.fileSettings.defaultLengthUnit))
|
||||
setLengthSetting(
|
||||
kclManager.fileSettings.defaultLengthUnit ||
|
||||
settings.context.modeling.defaultUnit.current
|
||||
settings.modeling.defaultUnit.current
|
||||
)
|
||||
}, [
|
||||
kclManager.fileSettings.defaultLengthUnit,
|
||||
settings.context.modeling.defaultUnit.current,
|
||||
settings.modeling.defaultUnit.current,
|
||||
])
|
||||
|
||||
return (
|
||||
<Popover className="relative pointer-events-auto">
|
||||
{({ close }) => (
|
||||
@ -75,7 +76,7 @@ export function UnitsMenu() {
|
||||
.catch(reportRejection)
|
||||
}
|
||||
} else {
|
||||
settings.send({
|
||||
settingsActor.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
createRoutesFromElements,
|
||||
} from 'react-router-dom'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
@ -120,12 +119,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// https://reactrouter.com/en/6.16.0/routers/picking-a-router#using-v64-data-apis
|
||||
const router = createMemoryRouter(
|
||||
createRoutesFromElements(
|
||||
<Route
|
||||
path="/file/:id"
|
||||
element={
|
||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||
}
|
||||
/>
|
||||
<Route path="/file/:id" element={<>{children}</>} />
|
||||
),
|
||||
{
|
||||
initialEntries: ['/file/new'],
|
||||
|
Reference in New Issue
Block a user