[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>
@ -13,8 +13,8 @@ import {
|
|||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
import { expectPixelColor } from './fixtures/sceneFixture'
|
import { expectPixelColor } from './fixtures/sceneFixture'
|
||||||
|
|
||||||
// Because onboarding relies on an app setting we need to set it as incompletel
|
// Because our default test settings have the onboardingStatus set to 'dismissed',
|
||||||
// for all these tests.
|
// we must set it to empty for the tests where we want to see the onboarding immediately.
|
||||||
|
|
||||||
test.describe('Onboarding tests', () => {
|
test.describe('Onboarding tests', () => {
|
||||||
test(
|
test(
|
||||||
@ -22,7 +22,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
{
|
{
|
||||||
appSettings: {
|
appSettings: {
|
||||||
app: {
|
app: {
|
||||||
onboardingStatus: 'incomplete',
|
onboardingStatus: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cleanProjectDir: true,
|
cleanProjectDir: true,
|
||||||
@ -63,7 +63,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
tag: '@electron',
|
tag: '@electron',
|
||||||
appSettings: {
|
appSettings: {
|
||||||
app: {
|
app: {
|
||||||
onboardingStatus: 'incomplete',
|
onboardingStatus: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cleanProjectDir: true,
|
cleanProjectDir: true,
|
||||||
@ -106,11 +106,6 @@ test.describe('Onboarding tests', () => {
|
|||||||
test(
|
test(
|
||||||
'Code resets after confirmation',
|
'Code resets after confirmation',
|
||||||
{
|
{
|
||||||
appSettings: {
|
|
||||||
app: {
|
|
||||||
onboardingStatus: 'incomplete',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cleanProjectDir: true,
|
cleanProjectDir: true,
|
||||||
},
|
},
|
||||||
async ({ context, page, homePage }) => {
|
async ({ context, page, homePage }) => {
|
||||||
@ -158,7 +153,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
{
|
{
|
||||||
appSettings: {
|
appSettings: {
|
||||||
app: {
|
app: {
|
||||||
onboardingStatus: 'incomplete',
|
onboardingStatus: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -319,7 +314,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
{
|
{
|
||||||
appSettings: {
|
appSettings: {
|
||||||
app: {
|
app: {
|
||||||
onboardingStatus: 'incomplete',
|
onboardingStatus: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cleanProjectDir: true,
|
cleanProjectDir: true,
|
||||||
@ -392,7 +387,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
{
|
{
|
||||||
appSettings: {
|
appSettings: {
|
||||||
app: {
|
app: {
|
||||||
onboardingStatus: 'incomplete',
|
onboardingStatus: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cleanProjectDir: true,
|
cleanProjectDir: true,
|
||||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@ -358,9 +358,7 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
|
|||||||
exact: true,
|
exact: true,
|
||||||
})
|
})
|
||||||
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||||
const mouseControlsSetting = page
|
const mouseControlsSetting = () => page.locator('#camera-controls').first()
|
||||||
.locator('#mouseControls')
|
|
||||||
.getByRole('combobox')
|
|
||||||
const mouseControlSuccesToast = page.getByText(
|
const mouseControlSuccesToast = page.getByText(
|
||||||
'Set mouse controls to "Solidworks"'
|
'Set mouse controls to "Solidworks"'
|
||||||
)
|
)
|
||||||
@ -390,7 +388,14 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
|
|||||||
await settingsLink.click()
|
await settingsLink.click()
|
||||||
await expect(settingsDialogHeading).toBeVisible()
|
await expect(settingsDialogHeading).toBeVisible()
|
||||||
await userSettingsTab.click()
|
await userSettingsTab.click()
|
||||||
await mouseControlsSetting.selectOption({ label: 'Solidworks' })
|
const setting = mouseControlsSetting()
|
||||||
|
await expect(setting).toBeAttached()
|
||||||
|
await setting.scrollIntoViewIfNeeded()
|
||||||
|
await setting.selectOption({ label: 'Solidworks' })
|
||||||
|
await expect(setting, 'Setting value did not change').toHaveValue(
|
||||||
|
'Solidworks',
|
||||||
|
{ timeout: 120_000 }
|
||||||
|
)
|
||||||
await expect(mouseControlSuccesToast).toBeVisible()
|
await expect(mouseControlSuccesToast).toBeVisible()
|
||||||
await settingsCloseButton.click()
|
await settingsCloseButton.click()
|
||||||
})
|
})
|
||||||
|
@ -633,6 +633,7 @@ test.describe('Testing settings', () => {
|
|||||||
`Set default unit to "${unitOfMeasure}" as a user default`
|
`Set default unit to "${unitOfMeasure}" as a user default`
|
||||||
)
|
)
|
||||||
await expect(toastMessage).toBeVisible()
|
await expect(toastMessage).toBeVisible()
|
||||||
|
await expect(toastMessage).not.toBeVisible()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await changeUnitOfMeasureInUserTab('in')
|
await changeUnitOfMeasureInUserTab('in')
|
||||||
|
@ -6,14 +6,12 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
|||||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||||
import { type IndexLoaderData } from 'lib/types'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
|
||||||
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
|
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
|
||||||
import { LowerRightControls } from 'components/LowerRightControls'
|
import { LowerRightControls } from 'components/LowerRightControls'
|
||||||
import ModalContainer from 'react-modal-promise'
|
import ModalContainer from 'react-modal-promise'
|
||||||
@ -30,6 +28,7 @@ import { useRouteLoaderData } from 'react-router-dom'
|
|||||||
import { useEngineCommands } from 'components/EngineCommands'
|
import { useEngineCommands } from 'components/EngineCommands'
|
||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
maybeWriteToDisk()
|
maybeWriteToDisk()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
@ -49,7 +48,6 @@ export function App() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
const { onProjectOpen } = useLspContext()
|
const { onProjectOpen } = useLspContext()
|
||||||
@ -71,7 +69,7 @@ export function App() {
|
|||||||
|
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
|
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const token = useToken()
|
const token = useToken()
|
||||||
|
|
||||||
const coreDumpManager = useMemo(
|
const coreDumpManager = useMemo(
|
||||||
@ -81,7 +79,7 @@ export function App() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
app: { onboardingStatus },
|
app: { onboardingStatus },
|
||||||
} = settings.context
|
} = settings
|
||||||
|
|
||||||
useHotkeys('backspace', (e) => {
|
useHotkeys('backspace', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -28,10 +28,8 @@ import {
|
|||||||
fileLoader,
|
fileLoader,
|
||||||
homeLoader,
|
homeLoader,
|
||||||
onboardingRedirectLoader,
|
onboardingRedirectLoader,
|
||||||
settingsLoader,
|
|
||||||
telemetryLoader,
|
telemetryLoader,
|
||||||
} from 'lib/routeLoaders'
|
} from 'lib/routeLoaders'
|
||||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
|
||||||
import LspProvider from 'components/LspProvider'
|
import LspProvider from 'components/LspProvider'
|
||||||
import { KclContextProvider } from 'lang/KclProvider'
|
import { KclContextProvider } from 'lang/KclProvider'
|
||||||
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
@ -45,34 +43,28 @@ import { AppStateProvider } from 'AppState'
|
|||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { RouteProvider } from 'components/RouteProvider'
|
import { RouteProvider } from 'components/RouteProvider'
|
||||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||||
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
|
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
|
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
|
|
||||||
const router = createRouter([
|
const router = createRouter([
|
||||||
{
|
{
|
||||||
loader: settingsLoader,
|
|
||||||
id: PATHS.INDEX,
|
id: PATHS.INDEX,
|
||||||
// TODO: Re-evaluate if this is true
|
|
||||||
/* Make sure auth is the outermost provider or else we will have
|
|
||||||
* inefficient re-renders, use the react profiler to see. */
|
|
||||||
element: (
|
element: (
|
||||||
<OpenInDesktopAppHandler>
|
<OpenInDesktopAppHandler>
|
||||||
<RouteProvider>
|
<RouteProvider>
|
||||||
<SettingsAuthProvider>
|
<LspProvider>
|
||||||
<LspProvider>
|
<ProjectsContextProvider>
|
||||||
<ProjectsContextProvider>
|
<KclContextProvider>
|
||||||
<KclContextProvider>
|
<AppStateProvider>
|
||||||
<AppStateProvider>
|
<MachineManagerProvider>
|
||||||
<MachineManagerProvider>
|
<Outlet />
|
||||||
<Outlet />
|
</MachineManagerProvider>
|
||||||
</MachineManagerProvider>
|
</AppStateProvider>
|
||||||
</AppStateProvider>
|
</KclContextProvider>
|
||||||
</KclContextProvider>
|
</ProjectsContextProvider>
|
||||||
</ProjectsContextProvider>
|
</LspProvider>
|
||||||
</LspProvider>
|
|
||||||
</SettingsAuthProvider>
|
|
||||||
</RouteProvider>
|
</RouteProvider>
|
||||||
</OpenInDesktopAppHandler>
|
</OpenInDesktopAppHandler>
|
||||||
),
|
),
|
||||||
@ -120,7 +112,6 @@ const router = createRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: PATHS.FILE + 'SETTINGS',
|
id: PATHS.FILE + 'SETTINGS',
|
||||||
loader: settingsLoader,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
loader: onboardingRedirectLoader,
|
loader: onboardingRedirectLoader,
|
||||||
@ -166,11 +157,9 @@ const router = createRouter([
|
|||||||
index: true,
|
index: true,
|
||||||
element: <></>,
|
element: <></>,
|
||||||
id: PATHS.HOME + 'SETTINGS',
|
id: PATHS.HOME + 'SETTINGS',
|
||||||
loader: settingsLoader,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: makeUrlPathRelative(PATHS.SETTINGS),
|
path: makeUrlPathRelative(PATHS.SETTINGS),
|
||||||
loader: settingsLoader,
|
|
||||||
element: <Settings />,
|
element: <Settings />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,6 @@ import { useRef, useEffect, useState, useMemo, Fragment } from 'react'
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||||
import { ReactCameraProperties } from './CameraControls'
|
import { ReactCameraProperties } from './CameraControls'
|
||||||
import { throttle, toSync } from 'lib/utils'
|
import { throttle, toSync } from 'lib/utils'
|
||||||
@ -48,6 +47,7 @@ import { ActionButton } from 'components/ActionButton'
|
|||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||||
@ -76,8 +76,8 @@ export const ClientSideScene = ({
|
|||||||
cameraControls,
|
cameraControls,
|
||||||
}: {
|
}: {
|
||||||
cameraControls: ReturnType<
|
cameraControls: ReturnType<
|
||||||
typeof useSettingsAuthContext
|
typeof useSettings
|
||||||
>['settings']['context']['modeling']['mouseControls']['current']
|
>['modeling']['mouseControls']['current']
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLDivElement>(null)
|
const canvasRef = useRef<HTMLDivElement>(null)
|
||||||
const { state, send, context } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
|
@ -1,24 +1,22 @@
|
|||||||
import { Switch } from '@headlessui/react'
|
import { Switch } from '@headlessui/react'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export function CameraProjectionToggle() {
|
export function CameraProjectionToggle() {
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const isCameraProjectionPerspective =
|
const isCameraProjectionPerspective =
|
||||||
settings.context.modeling.cameraProjection.current === 'perspective'
|
settings.modeling.cameraProjection.current === 'perspective'
|
||||||
const [checked, setChecked] = useState(isCameraProjectionPerspective)
|
const [checked, setChecked] = useState(isCameraProjectionPerspective)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChecked(
|
setChecked(settings.modeling.cameraProjection.current === 'perspective')
|
||||||
settings.context.modeling.cameraProjection.current === 'perspective'
|
}, [settings.modeling.cameraProjection.current])
|
||||||
)
|
|
||||||
}, [settings.context.modeling.cameraProjection.current])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(newValue) => {
|
onChange={(newValue) => {
|
||||||
settings.send({
|
settingsActor.send({
|
||||||
type: 'set.modeling.cameraProjection',
|
type: 'set.modeling.cameraProjection',
|
||||||
data: {
|
data: {
|
||||||
level: 'user',
|
level: 'user',
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
|
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { getSystemTheme } from 'lib/theme'
|
import { getSystemTheme } from 'lib/theme'
|
||||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
@ -20,6 +19,7 @@ import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
|||||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
const machineContextSelector = (snapshot?: {
|
const machineContextSelector = (snapshot?: {
|
||||||
@ -42,7 +42,7 @@ function CommandBarKclInput({
|
|||||||
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||||
arg.name
|
arg.name
|
||||||
] as KclCommandValue | undefined
|
] as KclCommandValue | undefined
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const argMachineContext = useSelector(
|
const argMachineContext = useSelector(
|
||||||
arg.machineActor,
|
arg.machineActor,
|
||||||
machineContextSelector
|
machineContextSelector
|
||||||
@ -117,9 +117,9 @@ function CommandBarKclInput({
|
|||||||
: defaultValue.length,
|
: defaultValue.length,
|
||||||
},
|
},
|
||||||
theme:
|
theme:
|
||||||
settings.context.app.theme.current === 'system'
|
settings.app.theme.current === 'system'
|
||||||
? getSystemTheme()
|
? getSystemTheme()
|
||||||
: settings.context.app.theme.current,
|
: settings.app.theme.current,
|
||||||
extensions: [
|
extensions: [
|
||||||
varMentionsExtension,
|
varMentionsExtension,
|
||||||
EditorView.updateListener.of((vu: ViewUpdate) => {
|
EditorView.updateListener.of((vu: ViewUpdate) => {
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { Dialog } from '@headlessui/react'
|
import { Dialog } from '@headlessui/react'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
|
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
const DownloadAppBanner = () => {
|
const DownloadAppBanner = () => {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
|
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const [isBannerDismissed, setIsBannerDismissed] = useState(
|
const [isBannerDismissed, setIsBannerDismissed] = useState(
|
||||||
settings.context.app.dismissWebBanner.current || hasCreateFileParam
|
settings.app.dismissWebBanner.current
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
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 { 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 React, { createContext, useEffect, useMemo } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import {
|
import {
|
||||||
@ -27,9 +27,10 @@ import {
|
|||||||
getKclSamplesManifest,
|
getKclSamplesManifest,
|
||||||
KclSamplesManifestItem,
|
KclSamplesManifestItem,
|
||||||
} from 'lib/getKclSamplesManifest'
|
} from 'lib/getKclSamplesManifest'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { markOnce } from 'lib/performance'
|
import { markOnce } from 'lib/performance'
|
||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
|
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
@ -48,14 +49,51 @@ export const FileMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { settings } = useSettingsAuthContext()
|
const location = useLocation()
|
||||||
const token = useToken()
|
const token = useToken()
|
||||||
|
const settings = useSettings()
|
||||||
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
const { project, file } = projectData
|
const { project, file } = projectData
|
||||||
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
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(() => {
|
useEffect(() => {
|
||||||
markOnce('code/didLoadFile')
|
markOnce('code/didLoadFile')
|
||||||
async function fetchKclSamples() {
|
async function fetchKclSamples() {
|
||||||
@ -323,7 +361,7 @@ export const FileMachineProvider = ({
|
|||||||
authToken: token ?? '',
|
authToken: token ?? '',
|
||||||
projectData,
|
projectData,
|
||||||
settings: {
|
settings: {
|
||||||
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
|
defaultUnit: settings.modeling.defaultUnit.current ?? 'mm',
|
||||||
},
|
},
|
||||||
specialPropsForSampleCommand: {
|
specialPropsForSampleCommand: {
|
||||||
onSubmit: async (data) => {
|
onSubmit: async (data) => {
|
||||||
@ -345,7 +383,7 @@ export const FileMachineProvider = ({
|
|||||||
// Either way, we want to overwrite the defaultUnit project setting
|
// Either way, we want to overwrite the defaultUnit project setting
|
||||||
// with the sample's setting.
|
// with the sample's setting.
|
||||||
if (data.sampleUnits) {
|
if (data.sampleUnits) {
|
||||||
settings.send({
|
settingsActor.send({
|
||||||
type: 'set.modeling.defaultUnit',
|
type: 'set.modeling.defaultUnit',
|
||||||
data: {
|
data: {
|
||||||
level: 'project',
|
level: 'project',
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
@ -9,6 +8,7 @@ import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
|||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { settingsActor } from 'machines/appMachine'
|
||||||
|
|
||||||
const HelpMenuDivider = () => (
|
const HelpMenuDivider = () => (
|
||||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
<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 filePath = useAbsoluteFilePath()
|
||||||
const isInProject = location.pathname.includes(PATHS.FILE)
|
const isInProject = location.pathname.includes(PATHS.FILE)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { settings } = useSettingsAuthContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
@ -106,7 +105,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
|||||||
<HelpMenuItem
|
<HelpMenuItem
|
||||||
as="button"
|
as="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
settings.send({
|
settingsActor.send({
|
||||||
type: 'set.app.onboardingStatus',
|
type: 'set.app.onboardingStatus',
|
||||||
data: {
|
data: {
|
||||||
value: '',
|
value: '',
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
import { TEST, VITE_KC_API_BASE_URL } from 'env'
|
import { TEST, VITE_KC_API_BASE_URL } from 'env'
|
||||||
import { kcl } from 'editor/plugins/lsp/kcl/language'
|
import { kcl } from 'editor/plugins/lsp/kcl/language'
|
||||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { Extension } from '@codemirror/state'
|
import { Extension } from '@codemirror/state'
|
||||||
import { LanguageSupport } from '@codemirror/language'
|
import { LanguageSupport } from '@codemirror/language'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
@ -22,7 +22,6 @@ import {
|
|||||||
modelingMachineDefaultContext,
|
modelingMachineDefaultContext,
|
||||||
} from 'machines/modelingMachine'
|
} from 'machines/modelingMachine'
|
||||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import {
|
import {
|
||||||
isCursorInSketchCommandRange,
|
isCursorInSketchCommandRange,
|
||||||
updateSketchDetailsNodePaths,
|
updateSketchDetailsNodePaths,
|
||||||
@ -110,6 +109,7 @@ import { kclEditorActor } from 'machines/kclEditorMachine'
|
|||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -131,19 +131,15 @@ export const ModelingMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
settings: {
|
app: { theme, enableSSAO, allowOrbitInSketchMode },
|
||||||
context: {
|
modeling: {
|
||||||
app: { theme, enableSSAO, allowOrbitInSketchMode },
|
defaultUnit,
|
||||||
modeling: {
|
cameraProjection,
|
||||||
defaultUnit,
|
highlightEdges,
|
||||||
cameraProjection,
|
showScaleGrid,
|
||||||
highlightEdges,
|
cameraOrbit,
|
||||||
showScaleGrid,
|
|
||||||
cameraOrbit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettings()
|
||||||
const previousAllowOrbitInSketchMode = useRef(allowOrbitInSketchMode.current)
|
const previousAllowOrbitInSketchMode = useRef(allowOrbitInSketchMode.current)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { context, send: fileMachineSend } = useFileContext()
|
const { context, send: fileMachineSend } = useFileContext()
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import styles from './ModelingPane.module.css'
|
import styles from './ModelingPane.module.css'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
export interface ModelingPaneProps {
|
export interface ModelingPaneProps {
|
||||||
id: string
|
id: string
|
||||||
@ -68,8 +68,8 @@ export const ModelingPane = ({
|
|||||||
title,
|
title,
|
||||||
...props
|
...props
|
||||||
}: ModelingPaneProps) => {
|
}: ModelingPaneProps) => {
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.app.onboardingStatus
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
onboardingStatus.current === onboardingPaths.CAMERA
|
onboardingStatus.current === onboardingPaths.CAMERA
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { TEST } from 'env'
|
import { TEST } from 'env'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||||
@ -51,6 +50,7 @@ import {
|
|||||||
} from 'machines/kclEditorMachine'
|
} from 'machines/kclEditorMachine'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { modelingMachineEvent } from 'editor/manager'
|
import { modelingMachineEvent } from 'editor/manager'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -63,9 +63,7 @@ export const editorShortcutMeta = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const KclEditorPane = () => {
|
export const KclEditorPane = () => {
|
||||||
const {
|
const context = useSettings()
|
||||||
settings: { context },
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
|
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
|
||||||
const editorIsMounted = useSelector(kclEditorActor, editorIsMountedSelector)
|
const editorIsMounted = useSelector(kclEditorActor, editorIsMountedSelector)
|
||||||
const theme =
|
const theme =
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { Resizable } from 're-resizable'
|
import { Resizable } from 're-resizable'
|
||||||
import {
|
import {
|
||||||
MouseEventHandler,
|
MouseEventHandler,
|
||||||
@ -21,6 +20,7 @@ import { MachineManagerContext } from 'components/MachineManagerProvider'
|
|||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -38,23 +38,23 @@ function getPlatformString(): 'web' | 'desktop' {
|
|||||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const kclContext = useKclContext()
|
const kclContext = useKclContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.app.onboardingStatus
|
||||||
const { send, context } = useModelingContext()
|
const { send, context } = useModelingContext()
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
onboardingStatus.current === onboardingPaths.CAMERA ||
|
onboardingStatus.current === onboardingPaths.CAMERA ||
|
||||||
context.store?.openPanes.length === 0
|
context.store?.openPanes.length === 0
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto '
|
: 'pointer-events-auto '
|
||||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
const showDebugPanel = settings.modeling.showDebugPanel
|
||||||
|
|
||||||
const paneCallbackProps = useMemo(
|
const paneCallbackProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
kclContext,
|
kclContext,
|
||||||
settings: settings.context,
|
settings,
|
||||||
platform: getPlatformString(),
|
platform: getPlatformString(),
|
||||||
}),
|
}),
|
||||||
[kclContext.diagnostics, settings.context]
|
[kclContext.diagnostics, settings]
|
||||||
)
|
)
|
||||||
|
|
||||||
const sidebarActions: SidebarAction[] = [
|
const sidebarActions: SidebarAction[] = [
|
||||||
@ -144,7 +144,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [settings.context])
|
}, [settings.modeling.showDebugPanel])
|
||||||
|
|
||||||
const togglePane = useCallback(
|
const togglePane = useCallback(
|
||||||
(newPane: SidebarType) => {
|
(newPane: SidebarType) => {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
|
||||||
import {
|
import {
|
||||||
NETWORK_HEALTH_TEXT,
|
NETWORK_HEALTH_TEXT,
|
||||||
NetworkHealthIndicator,
|
NetworkHealthIndicator,
|
||||||
@ -9,11 +8,7 @@ import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
|||||||
|
|
||||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
// wrap in router and xState context
|
// wrap in router and xState context
|
||||||
return (
|
return <BrowserRouter>{children}</BrowserRouter>
|
||||||
<BrowserRouter>
|
|
||||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
|
||||||
</BrowserRouter>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Our Playwright tests for this are much more comprehensive.
|
// Our Playwright tests for this are much more comprehensive.
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@ -32,9 +31,7 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Disables popover menu by default', () => {
|
test('Disables popover menu by default', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<SettingsAuthProviderJest>
|
<ProjectSidebarMenu project={projectWellFormed} />
|
||||||
<ProjectSidebarMenu project={projectWellFormed} />
|
|
||||||
</SettingsAuthProviderJest>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ import { SnapshotFrom } from 'xstate'
|
|||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { copyFileShareLink } from 'lib/links'
|
import { copyFileShareLink } from 'lib/links'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
@ -103,7 +102,6 @@ function ProjectMenuPopover({
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
useSettingsAuthContext()
|
|
||||||
const token = useToken()
|
const token = useToken()
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const commands = useSelector(commandBarActor, commandsSelector)
|
const commands = useSelector(commandBarActor, commandsSelector)
|
||||||
|
@ -20,11 +20,11 @@ import {
|
|||||||
getUniqueProjectName,
|
getUniqueProjectName,
|
||||||
getNextFileName,
|
getNextFileName,
|
||||||
} from 'lib/desktopFS'
|
} from 'lib/desktopFS'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||||
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
import {
|
import {
|
||||||
CREATE_FILE_URL_PARAM,
|
CREATE_FILE_URL_PARAM,
|
||||||
FILE_EXT,
|
FILE_EXT,
|
||||||
@ -77,9 +77,7 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
|||||||
searchParams.delete('units')
|
searchParams.delete('units')
|
||||||
setSearchParams(searchParams)
|
setSearchParams(searchParams)
|
||||||
}, [searchParams, setSearchParams])
|
}, [searchParams, setSearchParams])
|
||||||
const {
|
const settings = useSettings()
|
||||||
settings: { context: settings },
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
|
|
||||||
const [state, send, actor] = useMachine(
|
const [state, send, actor] = useMachine(
|
||||||
projectsMachine.provide({
|
projectsMachine.provide({
|
||||||
@ -183,9 +181,7 @@ const ProjectsContextDesktop = ({
|
|||||||
setSearchParams(searchParams)
|
setSearchParams(searchParams)
|
||||||
}, [searchParams, setSearchParams])
|
}, [searchParams, setSearchParams])
|
||||||
const { onProjectOpen } = useLspContext()
|
const { onProjectOpen } = useLspContext()
|
||||||
const {
|
const settings = useSettings()
|
||||||
settings: { context: settings },
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
|
|
||||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
const { projectPaths, projectsDir } = useProjectsLoader([
|
||||||
|
@ -5,7 +5,6 @@ import { codeManager, engineCommandManager } from 'lib/singletons'
|
|||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
|
@ -1,17 +1,36 @@
|
|||||||
import { useEffect, useState, createContext, ReactNode } from 'react'
|
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 { PATHS } from 'lib/paths'
|
||||||
import { markOnce } from 'lib/performance'
|
import { markOnce } from 'lib/performance'
|
||||||
import { useAuthNavigation } from 'hooks/useAuthNavigation'
|
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 const RouteProviderContext = createContext({})
|
||||||
|
|
||||||
export function RouteProvider({ children }: { children: ReactNode }) {
|
export function RouteProvider({ children }: { children: ReactNode }) {
|
||||||
useAuthNavigation()
|
useAuthNavigation()
|
||||||
|
const loadedProject = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
const [first, setFirstState] = useState(true)
|
const [first, setFirstState] = useState(true)
|
||||||
|
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
|
const authState = useAuthState()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// On initialization, the react-router-dom does not send a 'loading' state event.
|
// On initialization, the react-router-dom does not send a 'loading' state event.
|
||||||
// it sends an idle event first.
|
// it sends an idle event first.
|
||||||
@ -28,6 +47,41 @@ export function RouteProvider({ children }: { children: ReactNode }) {
|
|||||||
setFirstState(false)
|
setFirstState(false)
|
||||||
}, [navigation])
|
}, [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 (
|
return (
|
||||||
<RouteProviderContext.Provider value={{}}>
|
<RouteProviderContext.Provider value={{}}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { Setting } from 'lib/settings/initialSettings'
|
import { Setting } from 'lib/settings/initialSettings'
|
||||||
import { SetEventTypes, SettingsLevel } from 'lib/settings/settingsTypes'
|
import { SetEventTypes, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||||
import {
|
import {
|
||||||
@ -25,6 +24,8 @@ import { useLspContext } from 'components/LspProvider'
|
|||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
interface AllSettingsFieldsProps {
|
interface AllSettingsFieldsProps {
|
||||||
searchParamTab: SettingsLevel
|
searchParamTab: SettingsLevel
|
||||||
@ -40,9 +41,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { onProjectOpen } = useLspContext()
|
const { onProjectOpen } = useLspContext()
|
||||||
const dotDotSlash = useDotDotSlash()
|
const dotDotSlash = useDotDotSlash()
|
||||||
const {
|
const context = useSettings()
|
||||||
settings: { send, context, state },
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
|
|
||||||
const projectPath = useMemo(() => {
|
const projectPath = useMemo(() => {
|
||||||
const filteredPathname = location.pathname
|
const filteredPathname = location.pathname
|
||||||
@ -62,7 +61,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
}, [location.pathname])
|
}, [location.pathname])
|
||||||
|
|
||||||
function restartOnboarding() {
|
function restartOnboarding() {
|
||||||
send({
|
settingsActor.send({
|
||||||
type: `set.app.onboardingStatus`,
|
type: `set.app.onboardingStatus`,
|
||||||
data: { level: 'user', value: '' },
|
data: { level: 'user', value: '' },
|
||||||
})
|
})
|
||||||
@ -72,11 +71,14 @@ export const AllSettingsFields = forwardRef(
|
|||||||
* A "listener" for the XState to return to "idle" state
|
* A "listener" for the XState to return to "idle" state
|
||||||
* when the user resets the onboarding, using the callback above
|
* when the user resets the onboarding, using the callback above
|
||||||
*/
|
*/
|
||||||
|
const isSettingsMachineIdle = useSelector(settingsActor, (s) =>
|
||||||
|
s.matches('idle')
|
||||||
|
)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function navigateToOnboardingStart() {
|
async function navigateToOnboardingStart() {
|
||||||
if (
|
if (
|
||||||
state.context.app.onboardingStatus.user === '' &&
|
context.app.onboardingStatus.current === '' &&
|
||||||
state.matches('idle')
|
isSettingsMachineIdle
|
||||||
) {
|
) {
|
||||||
if (isFileSettings) {
|
if (isFileSettings) {
|
||||||
// If we're in a project, first navigate to the onboarding start here
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
navigateToOnboardingStart()
|
navigateToOnboardingStart()
|
||||||
}, [isFileSettings, navigate, state])
|
}, [
|
||||||
|
isFileSettings,
|
||||||
|
navigate,
|
||||||
|
isSettingsMachineIdle,
|
||||||
|
context.app.onboardingStatus.current,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-y-auto">
|
<div className="relative overflow-y-auto">
|
||||||
@ -142,7 +149,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
}
|
}
|
||||||
parentLevel={setting.getParentLevel(searchParamTab)}
|
parentLevel={setting.getParentLevel(searchParamTab)}
|
||||||
onFallback={() =>
|
onFallback={() =>
|
||||||
send({
|
settingsActor.send({
|
||||||
type: `set.${category}.${settingName}`,
|
type: `set.${category}.${settingName}`,
|
||||||
data: {
|
data: {
|
||||||
level: searchParamTab,
|
level: searchParamTab,
|
||||||
@ -218,7 +225,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
send({
|
settingsActor.send({
|
||||||
type: 'Reset settings',
|
type: 'Reset settings',
|
||||||
level: searchParamTab,
|
level: searchParamTab,
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Toggle } from 'components/Toggle/Toggle'
|
import { Toggle } from 'components/Toggle/Toggle'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { Setting } from 'lib/settings/initialSettings'
|
import { Setting } from 'lib/settings/initialSettings'
|
||||||
import {
|
import {
|
||||||
SetEventTypes,
|
SetEventTypes,
|
||||||
@ -7,6 +6,7 @@ import {
|
|||||||
WildcardSetEvent,
|
WildcardSetEvent,
|
||||||
} from 'lib/settings/settingsTypes'
|
} from 'lib/settings/settingsTypes'
|
||||||
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
||||||
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { EventFrom } from 'xstate'
|
import { EventFrom } from 'xstate'
|
||||||
|
|
||||||
@ -25,9 +25,8 @@ export function SettingsFieldInput({
|
|||||||
settingsLevel,
|
settingsLevel,
|
||||||
setting,
|
setting,
|
||||||
}: SettingsFieldInputProps) {
|
}: SettingsFieldInputProps) {
|
||||||
const {
|
const context = useSettings()
|
||||||
settings: { context, send },
|
const send = settingsActor.send
|
||||||
} = useSettingsAuthContext()
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return setting.commandConfig &&
|
return setting.commandConfig &&
|
||||||
'options' in setting.commandConfig &&
|
'options' in setting.commandConfig &&
|
||||||
|
@ -2,10 +2,10 @@ import { Combobox } from '@headlessui/react'
|
|||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { interactionMap } from 'lib/settings/initialKeybindings'
|
import { interactionMap } from 'lib/settings/initialKeybindings'
|
||||||
import { Setting } from 'lib/settings/initialSettings'
|
import { Setting } from 'lib/settings/initialSettings'
|
||||||
import { SettingsLevel } from 'lib/settings/settingsTypes'
|
import { SettingsLevel } from 'lib/settings/settingsTypes'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
@ -32,23 +32,22 @@ export function SettingsSearchBar() {
|
|||||||
)
|
)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const settingsAsSearchable: SettingsSearchItem[] = useMemo(
|
const settingsAsSearchable: SettingsSearchItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
...Object.entries(settings.state.context).flatMap(
|
...Object.entries(settings).flatMap(([category, categorySettings]) =>
|
||||||
([category, categorySettings]) =>
|
Object.entries(categorySettings).flatMap(([settingName, setting]) => {
|
||||||
Object.entries(categorySettings).flatMap(([settingName, setting]) => {
|
const s = setting
|
||||||
const s = setting as Setting
|
return (['project', 'user'] satisfies SettingsLevel[])
|
||||||
return (['project', 'user'] satisfies SettingsLevel[])
|
.filter((l) => s.hideOnLevel !== l)
|
||||||
.filter((l) => s.hideOnLevel !== l)
|
.map((l) => ({
|
||||||
.map((l) => ({
|
category: decamelize(category, { separator: ' ' }),
|
||||||
category: decamelize(category, { separator: ' ' }),
|
name: settingName,
|
||||||
name: settingName,
|
description: s.description ?? '',
|
||||||
description: s.description ?? '',
|
displayName: decamelize(settingName, { separator: ' ' }),
|
||||||
displayName: decamelize(settingName, { separator: ' ' }),
|
level: l,
|
||||||
level: l as ExtendedSettingsLevel,
|
}))
|
||||||
}))
|
})
|
||||||
})
|
|
||||||
),
|
),
|
||||||
...Object.entries(interactionMap).flatMap(
|
...Object.entries(interactionMap).flatMap(
|
||||||
([category, categoryKeybindings]) =>
|
([category, categoryKeybindings]) =>
|
||||||
@ -61,7 +60,7 @@ export function SettingsSearchBar() {
|
|||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
[settings.state.context]
|
[settings]
|
||||||
)
|
)
|
||||||
const [searchResults, setSearchResults] = useState(settingsAsSearchable)
|
const [searchResults, setSearchResults] = useState(settingsAsSearchable)
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { Setting } from 'lib/settings/initialSettings'
|
import { Setting } from 'lib/settings/initialSettings'
|
||||||
import { SettingsLevel } from 'lib/settings/settingsTypes'
|
import { SettingsLevel } from 'lib/settings/settingsTypes'
|
||||||
import { shouldHideSetting } from 'lib/settings/settingsUtils'
|
import { shouldHideSetting } from 'lib/settings/settingsUtils'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
interface SettingsSectionsListProps {
|
interface SettingsSectionsListProps {
|
||||||
searchParamTab: SettingsLevel
|
searchParamTab: SettingsLevel
|
||||||
@ -13,9 +13,7 @@ export function SettingsSectionsList({
|
|||||||
searchParamTab,
|
searchParamTab,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
}: SettingsSectionsListProps) {
|
}: SettingsSectionsListProps) {
|
||||||
const {
|
const context = useSettings()
|
||||||
settings: { context },
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
return (
|
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">
|
<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)
|
{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 { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
@ -20,8 +19,8 @@ import { IndexLoaderData } from 'lib/types'
|
|||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
import { useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
enum StreamState {
|
enum StreamState {
|
||||||
Playing = 'playing',
|
Playing = 'playing',
|
||||||
@ -34,7 +33,7 @@ export const Stream = () => {
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const { state, send } = useModelingContext()
|
const { state, send } = useModelingContext()
|
||||||
const commandBarState = useCommandBarState()
|
const commandBarState = useCommandBarState()
|
||||||
const { mediaStream } = useAppStream()
|
const { mediaStream } = useAppStream()
|
||||||
@ -42,7 +41,7 @@ export const Stream = () => {
|
|||||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
const [streamState, setStreamState] = useState(StreamState.Unset)
|
||||||
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
|
|
||||||
const IDLE = settings.context.app.streamIdleMode.current
|
const IDLE = settings.app.streamIdleMode.current
|
||||||
|
|
||||||
const isNetworkOkay =
|
const isNetworkOkay =
|
||||||
overallState === NetworkHealthState.Ok ||
|
overallState === NetworkHealthState.Ok ||
|
||||||
@ -336,7 +335,7 @@ export const Stream = () => {
|
|||||||
id="video-stream"
|
id="video-stream"
|
||||||
/>
|
/>
|
||||||
<ClientSideScene
|
<ClientSideScene
|
||||||
cameraControls={settings.context.modeling.mouseControls.current}
|
cameraControls={settings.modeling.mouseControls.current}
|
||||||
/>
|
/>
|
||||||
{(streamState === StreamState.Paused ||
|
{(streamState === StreamState.Paused ||
|
||||||
streamState === StreamState.Resuming) && (
|
streamState === StreamState.Resuming) && (
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
import { changeKclSettings, unitLengthToUnitLen } from 'lang/wasm'
|
import { changeKclSettings, unitLengthToUnitLen } from 'lang/wasm'
|
||||||
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||||
import { codeManager, kclManager } from 'lib/singletons'
|
import { codeManager, kclManager } from 'lib/singletons'
|
||||||
@ -8,24 +8,25 @@ import { useEffect, useState } from 'react'
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
export function UnitsMenu() {
|
export function UnitsMenu() {
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
const [hasPerFileLengthUnit, setHasPerFileLengthUnit] = useState(
|
const [hasPerFileLengthUnit, setHasPerFileLengthUnit] = useState(
|
||||||
Boolean(kclManager.fileSettings.defaultLengthUnit)
|
Boolean(kclManager.fileSettings.defaultLengthUnit)
|
||||||
)
|
)
|
||||||
const [lengthSetting, setLengthSetting] = useState(
|
const [lengthSetting, setLengthSetting] = useState(
|
||||||
kclManager.fileSettings.defaultLengthUnit ||
|
kclManager.fileSettings.defaultLengthUnit ||
|
||||||
settings.context.modeling.defaultUnit.current
|
settings.modeling.defaultUnit.current
|
||||||
)
|
)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasPerFileLengthUnit(Boolean(kclManager.fileSettings.defaultLengthUnit))
|
setHasPerFileLengthUnit(Boolean(kclManager.fileSettings.defaultLengthUnit))
|
||||||
setLengthSetting(
|
setLengthSetting(
|
||||||
kclManager.fileSettings.defaultLengthUnit ||
|
kclManager.fileSettings.defaultLengthUnit ||
|
||||||
settings.context.modeling.defaultUnit.current
|
settings.modeling.defaultUnit.current
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
kclManager.fileSettings.defaultLengthUnit,
|
kclManager.fileSettings.defaultLengthUnit,
|
||||||
settings.context.modeling.defaultUnit.current,
|
settings.modeling.defaultUnit.current,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative pointer-events-auto">
|
<Popover className="relative pointer-events-auto">
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
@ -75,7 +76,7 @@ export function UnitsMenu() {
|
|||||||
.catch(reportRejection)
|
.catch(reportRejection)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settings.send({
|
settingsActor.send({
|
||||||
type: 'set.modeling.defaultUnit',
|
type: 'set.modeling.defaultUnit',
|
||||||
data: {
|
data: {
|
||||||
level: 'project',
|
level: 'project',
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
createRoutesFromElements,
|
createRoutesFromElements,
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
|
||||||
|
|
||||||
type User = Models['User_type']
|
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
|
// https://reactrouter.com/en/6.16.0/routers/picking-a-router#using-v64-data-apis
|
||||||
const router = createMemoryRouter(
|
const router = createMemoryRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
<Route
|
<Route path="/file/:id" element={<>{children}</>} />
|
||||||
path="/file/:id"
|
|
||||||
element={
|
|
||||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
initialEntries: ['/file/new'],
|
initialEntries: ['/file/new'],
|
||||||
|
@ -2,10 +2,10 @@ import { base64ToString } from 'lib/base64'
|
|||||||
import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants'
|
import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import { useSettingsAuthContext } from './useSettingsAuthContext'
|
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { FileLinkParams } from 'lib/links'
|
import { FileLinkParams } from 'lib/links'
|
||||||
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
|
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
// For initializing the command arguments, we actually want `method` to be undefined
|
// For initializing the command arguments, we actually want `method` to be undefined
|
||||||
// so that we don't skip it in the command palette.
|
// so that we don't skip it in the command palette.
|
||||||
@ -26,7 +26,7 @@ export function useCreateFileLinkQuery(
|
|||||||
callback: (args: CreateFileSchemaMethodOptional) => void
|
callback: (args: CreateFileSchemaMethodOptional) => void
|
||||||
) {
|
) {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const createFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
|
const createFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
|
||||||
@ -45,7 +45,7 @@ export function useCreateFileLinkQuery(
|
|||||||
? params.name.replace('.kcl', '')
|
? params.name.replace('.kcl', '')
|
||||||
: params.name
|
: params.name
|
||||||
: isDesktop()
|
: isDesktop()
|
||||||
? settings.context.projects.defaultProjectName.current
|
? settings.projects.defaultProjectName.current
|
||||||
: DEFAULT_FILE_NAME,
|
: DEFAULT_FILE_NAME,
|
||||||
code: params.code || '',
|
code: params.code || '',
|
||||||
method: isDesktop() ? undefined : 'existingProject',
|
method: isDesktop() ? undefined : 'existingProject',
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import { useRouteLoaderData } from 'react-router-dom'
|
|
||||||
import { useSettingsAuthContext } from './useSettingsAuthContext'
|
|
||||||
import { PATHS } from 'lib/paths'
|
|
||||||
import { settings } from 'lib/settings/initialSettings'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* I was dismayed to learn that index route in Router.tsx where we initially load up the settings
|
|
||||||
* doesn't re-run on subsequent navigations. This hook is a workaround,
|
|
||||||
* in conjunction with additional uses of settingsLoader further down the router tree.
|
|
||||||
* @param routeId - The id defined in Router.tsx to load the settings from.
|
|
||||||
*/
|
|
||||||
export function useRefreshSettings(routeId: string = PATHS.INDEX) {
|
|
||||||
const ctx = useSettingsAuthContext()
|
|
||||||
const routeData = useRouteLoaderData(routeId) as typeof settings
|
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
// Intended to stop the world
|
|
||||||
// eslint-disable-next-line
|
|
||||||
throw new Error(
|
|
||||||
'useRefreshSettings must be used within a SettingsAuthProvider'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
ctx.settings.send({
|
|
||||||
type: 'Set all settings',
|
|
||||||
settings: routeData,
|
|
||||||
doNotPersist: true,
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the current theme based on the theme setting
|
* Resolves the current theme based on the theme setting
|
||||||
@ -7,10 +7,8 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|||||||
* @returns {Themes.Light | Themes.Dark}
|
* @returns {Themes.Light | Themes.Dark}
|
||||||
*/
|
*/
|
||||||
export function useResolvedTheme() {
|
export function useResolvedTheme() {
|
||||||
const {
|
const settings = useSettings()
|
||||||
settings: { context },
|
return settings.app.theme.current === Themes.System
|
||||||
} = useSettingsAuthContext()
|
|
||||||
return context.app.theme.current === Themes.System
|
|
||||||
? getSystemTheme()
|
? getSystemTheme()
|
||||||
: context.app.theme.current
|
: settings.app.theme.current
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { SettingsAuthContext } from 'components/SettingsAuthProvider'
|
|
||||||
import { useContext } from 'react'
|
|
||||||
|
|
||||||
export const useSettingsAuthContext = () => {
|
|
||||||
return useContext(SettingsAuthContext)
|
|
||||||
}
|
|
@ -11,6 +11,9 @@ import { ToastUpdate } from 'components/ToastUpdate'
|
|||||||
import { markOnce } from 'lib/performance'
|
import { markOnce } from 'lib/performance'
|
||||||
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
||||||
import { initializeWindowExceptionHandler } from 'lib/exceptions'
|
import { initializeWindowExceptionHandler } from 'lib/exceptions'
|
||||||
|
import { initPromise } from 'lang/wasm'
|
||||||
|
import { appActor } from 'machines/appMachine'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
markOnce('code/willAuth')
|
markOnce('code/willAuth')
|
||||||
initializeWindowExceptionHandler()
|
initializeWindowExceptionHandler()
|
||||||
@ -23,6 +26,14 @@ initializeWindowExceptionHandler()
|
|||||||
// iframe: false,
|
// iframe: false,
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
// Don't start the app machine until all these singletons
|
||||||
|
// are initialized, and the wasm module is loaded.
|
||||||
|
initPromise
|
||||||
|
.then(() => {
|
||||||
|
appActor.start()
|
||||||
|
})
|
||||||
|
.catch(reportRejection)
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
import { type IndexLoaderData } from 'lib/types'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import { useLoaderData } from 'react-router-dom'
|
import { useRouteLoaderData } from 'react-router-dom'
|
||||||
import { codeManager, kclManager } from 'lib/singletons'
|
import { codeManager, kclManager } from 'lib/singletons'
|
||||||
import { Diagnostic } from '@codemirror/lint'
|
import { Diagnostic } from '@codemirror/lint'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
|
import { PATHS } from 'lib/paths'
|
||||||
|
|
||||||
const KclContext = createContext({
|
const KclContext = createContext({
|
||||||
code: codeManager?.code || '',
|
code: codeManager?.code || '',
|
||||||
@ -27,7 +28,9 @@ export function KclContextProvider({
|
|||||||
}) {
|
}) {
|
||||||
// If we try to use this component anywhere but under the paths.FILE route it will fail
|
// If we try to use this component anywhere but under the paths.FILE route it will fail
|
||||||
// Because useLoaderData assumes we are on within it's context.
|
// Because useLoaderData assumes we are on within it's context.
|
||||||
const { code: loadedCode } = useLoaderData() as IndexLoaderData
|
const data = useRouteLoaderData(PATHS.FILE) as IndexLoaderData | undefined
|
||||||
|
const loadedCode = data?.code
|
||||||
|
|
||||||
// Both the code state and the editor state start off with the same code.
|
// Both the code state and the editor state start off with the same code.
|
||||||
const [code, setCode] = useState(loadedCode || codeManager.code)
|
const [code, setCode] = useState(loadedCode || codeManager.code)
|
||||||
|
|
||||||
|
@ -463,11 +463,11 @@ export const executeWithEngine = async (
|
|||||||
const jsAppSettings = async () => {
|
const jsAppSettings = async () => {
|
||||||
let jsAppSettings = default_app_settings()
|
let jsAppSettings = default_app_settings()
|
||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
const lastSettingsSnapshot = await import(
|
const settings = await import('machines/appMachine').then((module) =>
|
||||||
'components/SettingsAuthProvider'
|
module.getSettings()
|
||||||
).then((module) => module.lastSettingsContextSnapshot)
|
)
|
||||||
if (lastSettingsSnapshot) {
|
if (settings) {
|
||||||
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
|
jsAppSettings = getAllCurrentSettings(settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return jsAppSettings
|
return jsAppSettings
|
||||||
|
@ -7,20 +7,23 @@ import {
|
|||||||
SettingsPaths,
|
SettingsPaths,
|
||||||
SettingsLevel,
|
SettingsLevel,
|
||||||
SettingProps,
|
SettingProps,
|
||||||
|
SetEventTypes,
|
||||||
} from 'lib/settings/settingsTypes'
|
} from 'lib/settings/settingsTypes'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { PathValue } from 'lib/types'
|
import { PathValue } from 'lib/types'
|
||||||
import { Actor, AnyStateMachine, ContextFrom } from 'xstate'
|
import { ActorRefFrom, AnyStateMachine } from 'xstate'
|
||||||
import { getPropertyByPath } from 'lib/objectPropertyByPath'
|
import { getPropertyByPath } from 'lib/objectPropertyByPath'
|
||||||
import { buildCommandArgument } from 'lib/createMachineCommand'
|
import { buildCommandArgument } from 'lib/createMachineCommand'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { Setting } from 'lib/settings/initialSettings'
|
import {
|
||||||
|
createSettings,
|
||||||
|
Setting,
|
||||||
|
SettingsType,
|
||||||
|
} from 'lib/settings/initialSettings'
|
||||||
|
|
||||||
// An array of the paths to all of the settings that have commandConfigs
|
// An array of the paths to all of the settings that have commandConfigs
|
||||||
export const settingsWithCommandConfigs = (
|
export const settingsWithCommandConfigs = (s: SettingsType) =>
|
||||||
s: ContextFrom<typeof settingsMachine>
|
|
||||||
) =>
|
|
||||||
Object.entries(s).flatMap(([categoryName, categorySettings]) =>
|
Object.entries(s).flatMap(([categoryName, categorySettings]) =>
|
||||||
Object.entries(categorySettings)
|
Object.entries(categorySettings)
|
||||||
.filter(([_, setting]) => setting.commandConfig !== undefined)
|
.filter(([_, setting]) => setting.commandConfig !== undefined)
|
||||||
@ -28,7 +31,7 @@ export const settingsWithCommandConfigs = (
|
|||||||
) as SettingsPaths[]
|
) as SettingsPaths[]
|
||||||
|
|
||||||
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
|
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
|
||||||
actor: Actor<T>,
|
actor: ActorRefFrom<T>,
|
||||||
isProjectAvailable: boolean,
|
isProjectAvailable: boolean,
|
||||||
hideOnLevel?: SettingsLevel
|
hideOnLevel?: SettingsLevel
|
||||||
): CommandArgument<SettingsLevel, T> => ({
|
): CommandArgument<SettingsLevel, T> => ({
|
||||||
@ -53,23 +56,16 @@ const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
|
|||||||
|
|
||||||
interface CreateSettingsArgs {
|
interface CreateSettingsArgs {
|
||||||
type: SettingsPaths
|
type: SettingsPaths
|
||||||
send: Function
|
actor: ActorRefFrom<typeof settingsMachine>
|
||||||
context: ContextFrom<typeof settingsMachine>
|
|
||||||
actor: Actor<typeof settingsMachine>
|
|
||||||
isProjectAvailable: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Takes a Setting with a commandConfig and creates a Command
|
// Takes a Setting with a commandConfig and creates a Command
|
||||||
// that can be used in the CommandBar component.
|
// that can be used in the CommandBar component.
|
||||||
export function createSettingsCommand({
|
export function createSettingsCommand({ type, actor }: CreateSettingsArgs) {
|
||||||
type,
|
type S = PathValue<ReturnType<typeof createSettings>, typeof type>
|
||||||
send,
|
|
||||||
context,
|
|
||||||
actor,
|
|
||||||
isProjectAvailable,
|
|
||||||
}: CreateSettingsArgs) {
|
|
||||||
type S = PathValue<typeof context, typeof type>
|
|
||||||
|
|
||||||
|
const context = actor.getSnapshot().context
|
||||||
|
const isProjectAvailable = context.currentProject !== undefined
|
||||||
const settingConfig = getPropertyByPath(context, type) as SettingProps<
|
const settingConfig = getPropertyByPath(context, type) as SettingProps<
|
||||||
S['default']
|
S['default']
|
||||||
>
|
>
|
||||||
@ -129,10 +125,18 @@ export function createSettingsCommand({
|
|||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
needsReview: false,
|
needsReview: false,
|
||||||
onSubmit: (data) => {
|
onSubmit: (data) => {
|
||||||
if (data !== undefined && data !== null) {
|
if (
|
||||||
send({ type: `set.${type}`, data })
|
data !== undefined &&
|
||||||
|
data !== null &&
|
||||||
|
'value' in data &&
|
||||||
|
'level' in data
|
||||||
|
) {
|
||||||
|
// TS would not let me get this to type properly
|
||||||
|
const coercedData = data as unknown as SetEventTypes['data']
|
||||||
|
actor.send({ type: `set.${type}`, data: coercedData })
|
||||||
} else {
|
} else {
|
||||||
send({ type })
|
console.error('Invalid data submitted to settings command', data)
|
||||||
|
return new Error('Invalid data submitted to settings command', data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
|
@ -4,6 +4,7 @@ import { Project, FileEntry } from 'lib/project'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
defaultAppSettings,
|
defaultAppSettings,
|
||||||
|
initPromise,
|
||||||
parseAppSettings,
|
parseAppSettings,
|
||||||
parseProjectSettings,
|
parseProjectSettings,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
@ -131,11 +132,20 @@ export async function createNewProjectDirectory(
|
|||||||
export async function listProjects(
|
export async function listProjects(
|
||||||
configuration?: DeepPartial<Configuration> | Error
|
configuration?: DeepPartial<Configuration> | Error
|
||||||
): Promise<Project[]> {
|
): Promise<Project[]> {
|
||||||
if (configuration === undefined) {
|
// Make sure we have wasm initialized.
|
||||||
configuration = await readAppSettingsFile()
|
const initializedResult = await initPromise
|
||||||
|
if (err(initializedResult)) {
|
||||||
|
return Promise.reject(initializedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err(configuration)) return Promise.reject(configuration)
|
if (configuration === undefined) {
|
||||||
|
configuration = await readAppSettingsFile().catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err(configuration) || !configuration) return Promise.reject(configuration)
|
||||||
const projectDir = await ensureProjectDirectoryExists(configuration)
|
const projectDir = await ensureProjectDirectoryExists(configuration)
|
||||||
const projects = []
|
const projects = []
|
||||||
if (!projectDir) return Promise.reject(new Error('projectDir was falsey'))
|
if (!projectDir) return Promise.reject(new Error('projectDir was falsey'))
|
||||||
|
@ -75,11 +75,11 @@ export async function getProjectMetaByRouteId(
|
|||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseProjectRoute(
|
export function parseProjectRoute(
|
||||||
configuration: DeepPartial<Configuration>,
|
configuration: DeepPartial<Configuration>,
|
||||||
id: string,
|
id: string,
|
||||||
pathlib: PlatformPath | undefined
|
pathlib: PlatformPath | undefined
|
||||||
): Promise<ProjectRoute> {
|
): ProjectRoute {
|
||||||
let projectName = null
|
let projectName = null
|
||||||
let projectPath = ''
|
let projectPath = ''
|
||||||
let currentFileName = null
|
let currentFileName = null
|
||||||
|
@ -13,37 +13,9 @@ import makeUrlPathRelative from './makeUrlPathRelative'
|
|||||||
import { codeManager } from 'lib/singletons'
|
import { codeManager } from 'lib/singletons'
|
||||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
import { getProjectInfo } from './desktop'
|
import { getProjectInfo } from './desktop'
|
||||||
import { createSettings } from './settings/initialSettings'
|
|
||||||
import { normalizeLineEndings } from 'lib/codeEditor'
|
import { normalizeLineEndings } from 'lib/codeEditor'
|
||||||
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
|
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
|
||||||
|
import { getSettings, settingsActor } from 'machines/appMachine'
|
||||||
// The root loader simply resolves the settings and any errors that
|
|
||||||
// occurred during the settings load
|
|
||||||
export const settingsLoader: LoaderFunction = async ({
|
|
||||||
params,
|
|
||||||
}): Promise<
|
|
||||||
ReturnType<typeof createSettings> | ReturnType<typeof redirect>
|
|
||||||
> => {
|
|
||||||
let { settings, configuration } = await loadAndValidateSettings()
|
|
||||||
|
|
||||||
// I don't love that we have to read the settings again here,
|
|
||||||
// but we need to get the project path to load the project settings
|
|
||||||
if (params.id) {
|
|
||||||
const projectPathData = await getProjectMetaByRouteId(
|
|
||||||
params.id,
|
|
||||||
configuration
|
|
||||||
)
|
|
||||||
if (projectPathData) {
|
|
||||||
const { projectPath } = projectPathData
|
|
||||||
const { settings: s } = await loadAndValidateSettings(
|
|
||||||
projectPath || undefined
|
|
||||||
)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
||||||
export const telemetryLoader: LoaderFunction = async ({
|
export const telemetryLoader: LoaderFunction = async ({
|
||||||
params,
|
params,
|
||||||
@ -53,7 +25,7 @@ export const telemetryLoader: LoaderFunction = async ({
|
|||||||
|
|
||||||
// Redirect users to the appropriate onboarding page if they haven't completed it
|
// Redirect users to the appropriate onboarding page if they haven't completed it
|
||||||
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||||
const { settings } = await loadAndValidateSettings()
|
const settings = getSettings()
|
||||||
const onboardingStatus: OnboardingStatus =
|
const onboardingStatus: OnboardingStatus =
|
||||||
settings.app.onboardingStatus.current || ''
|
settings.app.onboardingStatus.current || ''
|
||||||
const notEnRouteToOnboarding = !args.request.url.includes(
|
const notEnRouteToOnboarding = !args.request.url.includes(
|
||||||
@ -72,7 +44,7 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return settingsLoader(args)
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileLoader: LoaderFunction = async (
|
export const fileLoader: LoaderFunction = async (
|
||||||
@ -156,9 +128,17 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
? await getProjectInfo(projectPath)
|
? await getProjectInfo(projectPath)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
const project = maybeProjectInfo ?? defaultProjectData
|
||||||
|
|
||||||
|
// Fire off the event to load the project settings
|
||||||
|
settingsActor.send({
|
||||||
|
type: 'load.project',
|
||||||
|
project,
|
||||||
|
})
|
||||||
|
|
||||||
const projectData: IndexLoaderData = {
|
const projectData: IndexLoaderData = {
|
||||||
code,
|
code,
|
||||||
project: maybeProjectInfo ?? defaultProjectData,
|
project,
|
||||||
file: {
|
file: {
|
||||||
name: currentFileName || '',
|
name: currentFileName || '',
|
||||||
path: currentFilePath || '',
|
path: currentFilePath || '',
|
||||||
@ -197,5 +177,8 @@ export const homeLoader: LoaderFunction = async ({
|
|||||||
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
settingsActor.send({
|
||||||
|
type: 'clear.project',
|
||||||
|
})
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
@ -554,3 +554,4 @@ export function createSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const settings = createSettings()
|
export const settings = createSettings()
|
||||||
|
export type SettingsType = ReturnType<typeof createSettings>
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
|
|
||||||
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
|
||||||
import { err } from 'lib/trap'
|
|
||||||
import {
|
import {
|
||||||
defaultAppSettings,
|
defaultAppSettings,
|
||||||
defaultProjectSettings,
|
defaultProjectSettings,
|
||||||
@ -10,9 +6,8 @@ import {
|
|||||||
parseProjectSettings,
|
parseProjectSettings,
|
||||||
tomlStringify,
|
tomlStringify,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
|
||||||
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
|
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
|
||||||
import { appThemeToTheme } from 'lib/theme'
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
import {
|
import {
|
||||||
getInitialDefaultDir,
|
getInitialDefaultDir,
|
||||||
readAppSettingsFile,
|
readAppSettingsFile,
|
||||||
@ -20,9 +15,14 @@ import {
|
|||||||
writeAppSettingsFile,
|
writeAppSettingsFile,
|
||||||
writeProjectSettingsFile,
|
writeProjectSettingsFile,
|
||||||
} from 'lib/desktop'
|
} from 'lib/desktop'
|
||||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
|
||||||
|
import { appThemeToTheme } from 'lib/theme'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
import { DeepPartial } from 'lib/types'
|
import { DeepPartial } from 'lib/types'
|
||||||
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
|
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||||
|
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert from a rust settings struct into the JS settings struct.
|
* Convert from a rust settings struct into the JS settings struct.
|
||||||
@ -312,6 +312,22 @@ export function getAllCurrentSettings(
|
|||||||
return currentSettings
|
return currentSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearSettingsAtLevel(
|
||||||
|
allSettings: typeof settings,
|
||||||
|
level: SettingsLevel
|
||||||
|
) {
|
||||||
|
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
|
||||||
|
const categoryKey = category as keyof typeof settings
|
||||||
|
Object.entries(settingsCategory).forEach(
|
||||||
|
([_, settingValue]: [string, Setting]) => {
|
||||||
|
settingValue[level] = undefined
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return allSettings
|
||||||
|
}
|
||||||
|
|
||||||
export function setSettingsAtLevel(
|
export function setSettingsAtLevel(
|
||||||
allSettings: typeof settings,
|
allSettings: typeof settings,
|
||||||
level: SettingsLevel,
|
level: SettingsLevel,
|
||||||
|
@ -1,26 +1,39 @@
|
|||||||
import { ActorRefFrom, createActor, setup } from 'xstate'
|
import { ActorRefFrom, assign, createActor, setup, spawnChild } from 'xstate'
|
||||||
import { authMachine } from './authMachine'
|
import { authMachine } from './authMachine'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { ACTOR_IDS } from './machineConstants'
|
import { ACTOR_IDS } from './machineConstants'
|
||||||
|
import { settingsMachine } from './settingsMachine'
|
||||||
|
import { createSettings } from 'lib/settings/initialSettings'
|
||||||
|
|
||||||
|
const { AUTH, SETTINGS } = ACTOR_IDS
|
||||||
|
const appMachineActors = {
|
||||||
|
[AUTH]: authMachine,
|
||||||
|
[SETTINGS]: settingsMachine,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type AppMachineActors = {
|
||||||
|
[K in keyof typeof appMachineActors]: ActorRefFrom<
|
||||||
|
(typeof appMachineActors)[K]
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
const appMachine = setup({
|
const appMachine = setup({
|
||||||
actors: {
|
actors: appMachineActors,
|
||||||
[ACTOR_IDS.AUTH]: authMachine,
|
|
||||||
},
|
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5gF8A0IB2B7CdGgAoBbAQwGMALASwzAEp8QAHLWKgFyqw0YA9EAjACZ0AT0FDkU5EA */
|
/** @xstate-layout N4IgpgJg5mDOIC5gF8A0IB2B7CdGgAoBbAQwGMALASwzAEp8QAHLWKgFyqw0YA9EAjACZ0AT0FDkU5EA */
|
||||||
id: 'modeling-app',
|
id: 'modeling-app',
|
||||||
invoke: [
|
entry: [
|
||||||
{
|
spawnChild(AUTH, { id: AUTH, systemId: AUTH }),
|
||||||
src: ACTOR_IDS.AUTH,
|
spawnChild(SETTINGS, {
|
||||||
systemId: ACTOR_IDS.AUTH,
|
id: SETTINGS,
|
||||||
},
|
systemId: SETTINGS,
|
||||||
|
input: createSettings(),
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const appActor = createActor(appMachine).start()
|
export const appActor = createActor(appMachine)
|
||||||
|
export const authActor = appActor.system.get(AUTH) as ActorRefFrom<
|
||||||
export const authActor = appActor.system.get(ACTOR_IDS.AUTH) as ActorRefFrom<
|
|
||||||
typeof authMachine
|
typeof authMachine
|
||||||
>
|
>
|
||||||
export const useAuthState = () => useSelector(authActor, (state) => state)
|
export const useAuthState = () => useSelector(authActor, (state) => state)
|
||||||
@ -28,3 +41,17 @@ export const useToken = () =>
|
|||||||
useSelector(authActor, (state) => state.context.token)
|
useSelector(authActor, (state) => state.context.token)
|
||||||
export const useUser = () =>
|
export const useUser = () =>
|
||||||
useSelector(authActor, (state) => state.context.user)
|
useSelector(authActor, (state) => state.context.user)
|
||||||
|
|
||||||
|
export const settingsActor = appActor.system.get(SETTINGS) as ActorRefFrom<
|
||||||
|
typeof settingsMachine
|
||||||
|
>
|
||||||
|
export const getSettings = () => {
|
||||||
|
const { currentProject: _, ...settings } = settingsActor.getSnapshot().context
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
export const useSettings = () =>
|
||||||
|
useSelector(settingsActor, (state) => {
|
||||||
|
// We have to peel everything that isn't settings off
|
||||||
|
const { currentProject, ...settings } = state.context
|
||||||
|
return settings
|
||||||
|
})
|
||||||
|
@ -80,7 +80,7 @@ export const authMachine = setup({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOhzEwGsBJAMwBkB7KGCa-AYgkcJIIDdGlMGWwVKAWgA2zVhIIBtAAwBdRKAAOjWLgAuuHupAAPRAGYArAEYSADgu2AnGYBMLpVYBsZz7YA0IACeiG6OJM62tmZKLgDsno5KtvEAvikBaFh4hKTkVHRMLJDsHGAATmWMZSQaUui6tFWoouLSspDy+MpqSCBaOvqGvaYIljb2Tq7uXj7+QYgALFYW4clWy1ZmVgsWsZtpGRg4BMQkMkVsnIUABIwArrrdRv16BvhGI74LJBYW7o5WKJmKILObBUZeEgJP4LTxKMwIhZmBYLA4gTLHHJnWQEKAAeQeXB4IgEQhEGOyp3OUFxBN0CFJmHqb26T16L0G72GiCsSg8PyszkBCViTiUjgC4Jcnhc4SUsQcvgsoL2VjRFJOpGptMJ5Uq1Vq9UaZWaGqx2vw+IeDPwgiZnNZqme2leQ1An1s31+-0BCJBYJCLm+lk8CRl9hRyos6qOlK17QgdI4N0UTvZLs5Hx58NsJARuys0tDSl+AYQthsgNi0TMqt2LjVaPwjAgcCMZuIzoGbyzCAknkliH7Maympa+QYCfYXddXPdixcg4QvKUdk2u2iLkcsXhCRHmKpU7nfQzPe5CAsMpIXi8MvFKM8VliS5c1jzj53W3isNFqPS6NjMcLStXQZ0zc8ohsJI-kcFxXEcR9HAWF9gTzDxbCUXxAQWEsdn3ONsQuOkwLPedl22MIzFg3YP1gl9PG+bYvGsSxlUcRJozSFIgA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwAWQ9gBspuQCYAnAGYAHPYCsx+4ccAaEAE9E1q7YcoZyxrYR1m7mcrYAvnE+aFh4BMTk1LSQjExgAE55VHnYKmIAhuhkRQC2qcLikpDSDPJKSCBqGlo67QYI9gDs5tge5o6h5vau7oY+-v3mA9jWco4u5iu21ua2YcYJSRg4Eln0zJkABFQYrbqdmtoMun2GA7YjxuPmLqvGNh5zRCfJaOcyLUzuAYuFyGcwHEDJY6NCAAeQwTEuskUd3UDx6oD6Im2wUcAzkMJ2cjBxlMgIWLmwZLWljecjJTjh8IYVAgcF0iJxXUez0QIgGxhJZIpu2ptL8AWwtje1nCW2iq1shns8MRdXSlGRjEFeKevUQjkcy3sqwGHimbg83nlCF22GMytVUWMMUc8USCKO2BOdCN7Xu3VNBKMKsVFp2hm2vu+1id83slkVrgTxhcW0pNJ1geDkDR6GNEZFCAT1kZZLk9cMLltb0WdPMjewjjC1mzOZCtk5CSAA */
|
||||||
id: ACTOR_IDS.AUTH,
|
id: ACTOR_IDS.AUTH,
|
||||||
initial: 'checkIfLoggedIn',
|
initial: 'checkIfLoggedIn',
|
||||||
context: {
|
context: {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export const ACTOR_IDS = {
|
export const ACTOR_IDS = {
|
||||||
AUTH: 'auth',
|
AUTH: 'auth',
|
||||||
}
|
SETTINGS: 'settings',
|
||||||
|
} as const
|
||||||
|
@ -1,6 +1,25 @@
|
|||||||
import { assign, setup } from 'xstate'
|
import {
|
||||||
import { Themes, getSystemTheme, setThemeClass } from 'lib/theme'
|
AnyActorRef,
|
||||||
import { createSettings, settings } from 'lib/settings/initialSettings'
|
assign,
|
||||||
|
enqueueActions,
|
||||||
|
EventObject,
|
||||||
|
fromCallback,
|
||||||
|
fromPromise,
|
||||||
|
sendTo,
|
||||||
|
setup,
|
||||||
|
} from 'xstate'
|
||||||
|
import {
|
||||||
|
Themes,
|
||||||
|
darkModeMatcher,
|
||||||
|
getOppositeTheme,
|
||||||
|
getSystemTheme,
|
||||||
|
setThemeClass,
|
||||||
|
} from 'lib/theme'
|
||||||
|
import {
|
||||||
|
createSettings,
|
||||||
|
settings,
|
||||||
|
SettingsType,
|
||||||
|
} from 'lib/settings/initialSettings'
|
||||||
import {
|
import {
|
||||||
BaseUnit,
|
BaseUnit,
|
||||||
SetEventTypes,
|
SetEventTypes,
|
||||||
@ -9,16 +28,39 @@ import {
|
|||||||
WildcardSetEvent,
|
WildcardSetEvent,
|
||||||
} from 'lib/settings/settingsTypes'
|
} from 'lib/settings/settingsTypes'
|
||||||
import {
|
import {
|
||||||
|
clearSettingsAtLevel,
|
||||||
configurationToSettingsPayload,
|
configurationToSettingsPayload,
|
||||||
|
loadAndValidateSettings,
|
||||||
projectConfigurationToSettingsPayload,
|
projectConfigurationToSettingsPayload,
|
||||||
|
saveSettings,
|
||||||
setSettingsAtLevel,
|
setSettingsAtLevel,
|
||||||
} from 'lib/settings/settingsUtils'
|
} from 'lib/settings/settingsUtils'
|
||||||
import { sceneInfra } from 'lib/singletons'
|
import {
|
||||||
|
codeManager,
|
||||||
|
engineCommandManager,
|
||||||
|
kclManager,
|
||||||
|
sceneEntitiesManager,
|
||||||
|
sceneInfra,
|
||||||
|
} from 'lib/singletons'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import decamelize from 'decamelize'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { Project } from 'lib/project'
|
||||||
|
import {
|
||||||
|
createSettingsCommand,
|
||||||
|
settingsWithCommandConfigs,
|
||||||
|
} from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||||
|
import { Command } from 'lib/commandTypes'
|
||||||
|
import { commandBarActor } from './commandBarMachine'
|
||||||
|
|
||||||
|
type SettingsMachineContext = SettingsType & {
|
||||||
|
currentProject?: Project
|
||||||
|
}
|
||||||
|
|
||||||
export const settingsMachine = setup({
|
export const settingsMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
context: {} as ReturnType<typeof createSettings>,
|
context: {} as SettingsMachineContext,
|
||||||
input: {} as ReturnType<typeof createSettings>,
|
input: {} as SettingsMachineContext,
|
||||||
events: {} as (
|
events: {} as (
|
||||||
| WildcardSetEvent<SettingsPaths>
|
| WildcardSetEvent<SettingsPaths>
|
||||||
| SetEventTypes
|
| SetEventTypes
|
||||||
@ -35,16 +77,219 @@ export const settingsMachine = setup({
|
|||||||
level: SettingsLevel
|
level: SettingsLevel
|
||||||
}
|
}
|
||||||
| { type: 'Set all settings'; settings: typeof settings }
|
| { type: 'Set all settings'; settings: typeof settings }
|
||||||
|
| { type: 'load.project'; project?: Project }
|
||||||
|
| { type: 'clear.project' }
|
||||||
) & { doNotPersist?: boolean },
|
) & { doNotPersist?: boolean },
|
||||||
},
|
},
|
||||||
|
actors: {
|
||||||
|
persistSettings: fromPromise<
|
||||||
|
void,
|
||||||
|
{ doNotPersist: boolean; context: SettingsMachineContext }
|
||||||
|
>(async ({ input }) => {
|
||||||
|
// Without this, when a user changes the file, it'd
|
||||||
|
// create a detection loop with the file-system watcher.
|
||||||
|
if (input.doNotPersist) return
|
||||||
|
|
||||||
|
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
|
||||||
|
const { currentProject, ...settings } = input.context
|
||||||
|
|
||||||
|
return saveSettings(settings, currentProject?.path)
|
||||||
|
}),
|
||||||
|
loadUserSettings: fromPromise<SettingsMachineContext, void>(async () => {
|
||||||
|
const { settings } = await loadAndValidateSettings()
|
||||||
|
return settings
|
||||||
|
}),
|
||||||
|
loadProjectSettings: fromPromise<
|
||||||
|
SettingsMachineContext,
|
||||||
|
{ project?: Project }
|
||||||
|
>(async ({ input }) => {
|
||||||
|
const { settings } = await loadAndValidateSettings(input.project?.path)
|
||||||
|
return settings
|
||||||
|
}),
|
||||||
|
watchSystemTheme: fromCallback<{
|
||||||
|
type: 'update.themeWatcher'
|
||||||
|
theme: Themes
|
||||||
|
}>(({ receive }) => {
|
||||||
|
const listener = (e: MediaQueryListEvent) => {
|
||||||
|
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
|
||||||
|
}
|
||||||
|
|
||||||
|
receive((event) => {
|
||||||
|
if (event.type !== 'update.themeWatcher') {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if (event.theme === Themes.System) {
|
||||||
|
darkModeMatcher?.addEventListener('change', listener)
|
||||||
|
} else {
|
||||||
|
darkModeMatcher?.removeEventListener('change', listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => darkModeMatcher?.removeEventListener('change', listener)
|
||||||
|
}),
|
||||||
|
registerCommands: fromCallback<
|
||||||
|
{ type: 'update' },
|
||||||
|
{ settings: SettingsType; actor: AnyActorRef }
|
||||||
|
>(({ input, receive }) => {
|
||||||
|
// If the user wants to hide the settings commands
|
||||||
|
//from the command bar don't add them.
|
||||||
|
if (settings.commandBar.includeSettings.current === false) return
|
||||||
|
let commands: Command[] = []
|
||||||
|
|
||||||
|
const updateCommands = () =>
|
||||||
|
settingsWithCommandConfigs(input.settings)
|
||||||
|
.map((type) =>
|
||||||
|
createSettingsCommand({
|
||||||
|
type,
|
||||||
|
actor: input.actor,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.filter((c) => c !== null) as Command[]
|
||||||
|
const addCommands = () =>
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Add commands',
|
||||||
|
data: { commands: commands },
|
||||||
|
})
|
||||||
|
const removeCommands = () =>
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Remove commands',
|
||||||
|
data: { commands: commands },
|
||||||
|
})
|
||||||
|
|
||||||
|
receive((event) => {
|
||||||
|
if (event.type !== 'update') return
|
||||||
|
removeCommands()
|
||||||
|
commands = updateCommands()
|
||||||
|
addCommands()
|
||||||
|
})
|
||||||
|
|
||||||
|
commands = updateCommands()
|
||||||
|
addCommands()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeCommands()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setEngineTheme: () => {},
|
setClientSideSceneUnits: ({ context, event }) => {
|
||||||
setClientTheme: () => {},
|
const newBaseUnit =
|
||||||
'Execute AST': () => {},
|
event.type === 'set.modeling.defaultUnit'
|
||||||
toastSuccess: () => {},
|
? (event.data.value as BaseUnit)
|
||||||
setClientSideSceneUnits: () => {},
|
: context.modeling.defaultUnit.current
|
||||||
setAllowOrbitInSketchMode: () => {},
|
if (!sceneInfra) return
|
||||||
persistSettings: () => {},
|
sceneInfra.baseUnit = newBaseUnit
|
||||||
|
},
|
||||||
|
setEngineTheme: ({ context }) => {
|
||||||
|
if (engineCommandManager && context.app.theme.current) {
|
||||||
|
engineCommandManager
|
||||||
|
.setTheme(context.app.theme.current)
|
||||||
|
.catch(reportRejection)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setClientTheme: ({ context }) => {
|
||||||
|
if (!sceneInfra || !sceneEntitiesManager) return
|
||||||
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
|
sceneInfra.theme = opposingTheme
|
||||||
|
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
||||||
|
},
|
||||||
|
setAllowOrbitInSketchMode: ({ context }) => {
|
||||||
|
if (!sceneInfra.camControls) return
|
||||||
|
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 || context)
|
||||||
|
const resetSettingsIncludesUnitChange =
|
||||||
|
event.type === 'Reset settings' && relevantSetting(settings)
|
||||||
|
|
||||||
|
const shouldExecute =
|
||||||
|
kclManager !== undefined &&
|
||||||
|
(event.type === 'set.modeling.defaultUnit' ||
|
||||||
|
event.type === 'set.modeling.showScaleGrid' ||
|
||||||
|
event.type === 'set.modeling.highlightEdges' ||
|
||||||
|
allSettingsIncludesUnitChange ||
|
||||||
|
resetSettingsIncludesUnitChange)
|
||||||
|
|
||||||
|
if (shouldExecute) {
|
||||||
|
// Unit changes requires a re-exec of code
|
||||||
|
kclManager.executeCode(true).catch(reportRejection)
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setThemeColor: ({ context }) => {
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
`--primary-hue`,
|
||||||
|
context.app.themeColor.current
|
||||||
|
)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Update the --cursor-color CSS variable
|
||||||
|
* based on the setting textEditor.blinkingCursor.current
|
||||||
|
*/
|
||||||
|
setCursorColor: ({ context }) => {
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
`--cursor-color`,
|
||||||
|
context.textEditor.blinkingCursor.current ? 'auto' : 'transparent'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
/** Unload the project-level setting values from memory */
|
||||||
|
clearProjectSettings: assign(({ context }) => {
|
||||||
|
// Peel off all non-settings context
|
||||||
|
const { currentProject: _, ...settings } = context
|
||||||
|
const newSettings = clearSettingsAtLevel(settings, 'project')
|
||||||
|
return newSettings
|
||||||
|
}),
|
||||||
|
/** Unload the current project's info from memory */
|
||||||
|
clearCurrentProject: assign(({ context }) => {
|
||||||
|
return { ...context, currentProject: undefined }
|
||||||
|
}),
|
||||||
resetSettings: assign(({ context, event }) => {
|
resetSettings: assign(({ context, event }) => {
|
||||||
if (!('level' in event)) return {}
|
if (!('level' in event)) return {}
|
||||||
|
|
||||||
@ -59,9 +304,10 @@ export const settingsMachine = setup({
|
|||||||
|
|
||||||
return newSettings
|
return newSettings
|
||||||
}),
|
}),
|
||||||
setAllSettings: assign(({ event }) => {
|
setAllSettings: assign(({ event, context }) => {
|
||||||
if (!('settings' in event)) return {}
|
if ('settings' in event) return event.settings
|
||||||
return event.settings
|
else if ('output' in event) return event.output || context
|
||||||
|
else return context
|
||||||
}),
|
}),
|
||||||
setSettingAtLevel: assign(({ context, event }) => {
|
setSettingAtLevel: assign(({ context, event }) => {
|
||||||
if (!('data' in event)) return {}
|
if (!('data' in event)) return {}
|
||||||
@ -94,25 +340,55 @@ export const settingsMachine = setup({
|
|||||||
const newCurrentProjection = context.modeling.cameraProjection.current
|
const newCurrentProjection = context.modeling.cameraProjection.current
|
||||||
sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection)
|
sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection)
|
||||||
},
|
},
|
||||||
|
sendThemeToWatcher: sendTo('watchSystemTheme', ({ context }) => ({
|
||||||
|
type: 'update.themeWatcher',
|
||||||
|
theme: context.app.theme.current,
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IAzAA4x+AIyaAbJoCsAFl1njAJmOaANCACeiXQHZ1+a7bdWDATnUxawBfYIdUDB4CIlIKKjoGNAALMABbMABhRmJGDnEpJBBZeUVsZTUELR19IzMLUy97J0RfTXxDBr8DAxtdMSs-UPD0LFxoknJKNHxUxggwYh58eYAzakFiNABVbAV85WKFTCVCiqq9QxNzSxsm50qDU3wrMXV1V2M-bT8xV6GQCKjPCECZxaYJfDJNJgfaFQ6lcoabQXWrXBq3Bz3YzqPz4AyvL7qYw1TS6Az-QFREGxKY0ej4WBoDhgaipACSEwAsnMYZIDnIjidQGdkSS6jdbJjEK5zHi-PouqYiQZjBSRlSYpN4vTqMQcgB3ADyHBYCjZ2GQAGt0ABjJLc+awmQChGnJHVS7i9GS5oIQweVxueVWVz4mW6NWRMbUrXTWbzRa4fA21lgDjUAAKHEYACswDbSk6ii7jmU3ZVRZ60Y0pQg+rorPglQ2rKZ-FY3DLI0DxjSqPGFkskpgoElFqO0ABRaBwIvw0uIise1H1Gu+3Rk3R6Uydaz47qqsIA9XRzVkABKcHQAAIpj25yWhap3SirquMevAriTK4urYBhYViaN2GqghE166sQt4nngD4lAu5Zkni1yaKG2i6OoBgblYtY7sYniGNYmjqKYmjyropggaeoK0gOiZQAySSMPqyApqQADiHBEHBgplsKL5itWH73BYKr4OoLzmBhHahlRwJnjk1AQPgtDZnmBY8a6-EIKYVgePopEfKRmhAQ2xi1m8FyuCGnxmHhJFyb25AAFSaQh2nnIJ74+iJgZPK87ykmR-SmK4wFHpS0a0Gm8iMjw0FRngZAQMwYCENgABujDWvgkXAtFHCxUCCU9ggOBZSmhaSG5T4VKFuIkUEO7uPKfihaYtb6JZQR+J8Sq-iR4XDIlBAFUV8V3lEZBptmHAqcQAgrLkqS5TBo0xZgcW4CVURlZljCVaW+Q1Xxz46ciRhiFhBjvEEPmIKSVk2b1O4NA5EVrfgincLgWyUBwyWpelWU5XlBDfTwf1pntFUCEd1V8nCj6nWczyfPiYgfL4xhhZoqGdR8OhXT0xJiL1HxaI5X3sD9UBQwDM25PNi3LatI3U0pkP-TDB1w8wx2I868G1RomEEURGHWZjrydV0Hjym2bw3bY2LqFTEO4Fmub5mggPYGl5XZWlYMc7TWvqWgPOHfzCMFELvGLn035dGIPgYW1ui9bLv7PK8LxGGRFG6erNM8ObOvTRws3M2gS0cCtJsa1A4cFlbfPYALdvFsLKMaMS+BiKYfg2NigZk+85m+h2pg6L+MpthhKtEqER7YDy8CFGD-I54uAC06ie88ZL4i8FFfmSta92YBe-L8fhhaGLyYVTmrdw75ahbWqEGPgfjyj+PR762EYfezY2bcVk1jGvWlnWRTxB8YWEysY6O6Fve94vUB5fDXxIh5zX6-0b7uTOr3EMW4OzdH6K7JUZMJ7rh3I2X+PRpZvE+K4ABZs1I6xASLBAYhawdj6M8CSG4F64zVi3IAA */
|
||||||
id: 'Settings',
|
initial: 'loadingUser',
|
||||||
initial: 'idle',
|
|
||||||
context: ({ input }) => {
|
context: ({ input }) => {
|
||||||
return {
|
return {
|
||||||
...createSettings(),
|
...createSettings(),
|
||||||
...input,
|
...input,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
invoke: [
|
||||||
|
{
|
||||||
|
src: 'watchSystemTheme',
|
||||||
|
id: 'watchSystemTheme',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'registerCommands',
|
||||||
|
id: 'registerCommands',
|
||||||
|
// Peel off the non-settings context
|
||||||
|
input: ({ context: { currentProject, ...settings }, self }) => ({
|
||||||
|
settings,
|
||||||
|
actor: self,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
states: {
|
states: {
|
||||||
idle: {
|
idle: {
|
||||||
entry: ['setThemeClass', 'setClientSideSceneUnits'],
|
entry: ['setThemeClass', 'setClientSideSceneUnits', 'sendThemeToWatcher'],
|
||||||
|
|
||||||
on: {
|
on: {
|
||||||
'*': {
|
'*': {
|
||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
actions: ['setSettingAtLevel', 'toastSuccess'],
|
actions: [
|
||||||
|
'setSettingAtLevel',
|
||||||
|
'toastSuccess',
|
||||||
|
enqueueActions(({ enqueue, check }) => {
|
||||||
|
if (
|
||||||
|
check(
|
||||||
|
({ event }) => event.type === 'set.textEditor.blinkingCursor'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
enqueue('setCursorColor')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
'set.app.onboardingStatus': {
|
'set.app.onboardingStatus': {
|
||||||
@ -126,7 +402,7 @@ export const settingsMachine = setup({
|
|||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
|
|
||||||
// No toast
|
// No toast
|
||||||
actions: ['setSettingAtLevel'],
|
actions: ['setSettingAtLevel', 'setThemeColor'],
|
||||||
},
|
},
|
||||||
|
|
||||||
'set.modeling.defaultUnit': {
|
'set.modeling.defaultUnit': {
|
||||||
@ -149,6 +425,7 @@ export const settingsMachine = setup({
|
|||||||
'setThemeClass',
|
'setThemeClass',
|
||||||
'setEngineTheme',
|
'setEngineTheme',
|
||||||
'setClientTheme',
|
'setClientTheme',
|
||||||
|
'sendThemeToWatcher',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -191,9 +468,11 @@ export const settingsMachine = setup({
|
|||||||
'setThemeClass',
|
'setThemeClass',
|
||||||
'setEngineTheme',
|
'setEngineTheme',
|
||||||
'setClientSideSceneUnits',
|
'setClientSideSceneUnits',
|
||||||
|
'setThemeColor',
|
||||||
'Execute AST',
|
'Execute AST',
|
||||||
'setClientTheme',
|
'setClientTheme',
|
||||||
'setAllowOrbitInSketchMode',
|
'setAllowOrbitInSketchMode',
|
||||||
|
'sendThemeToWatcher',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -203,9 +482,11 @@ export const settingsMachine = setup({
|
|||||||
'setThemeClass',
|
'setThemeClass',
|
||||||
'setEngineTheme',
|
'setEngineTheme',
|
||||||
'setClientSideSceneUnits',
|
'setClientSideSceneUnits',
|
||||||
|
'setThemeColor',
|
||||||
'Execute AST',
|
'Execute AST',
|
||||||
'setClientTheme',
|
'setClientTheme',
|
||||||
'setAllowOrbitInSketchMode',
|
'setAllowOrbitInSketchMode',
|
||||||
|
'sendThemeToWatcher',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -213,12 +494,85 @@ export const settingsMachine = setup({
|
|||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
|
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'load.project': {
|
||||||
|
target: 'loadingProject',
|
||||||
|
},
|
||||||
|
|
||||||
|
'clear.project': {
|
||||||
|
target: 'idle',
|
||||||
|
reenter: true,
|
||||||
|
actions: [
|
||||||
|
'clearProjectSettings',
|
||||||
|
'clearCurrentProject',
|
||||||
|
'setThemeColor',
|
||||||
|
sendTo('registerCommands', { type: 'update' }),
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'persisting settings': {
|
'persisting settings': {
|
||||||
entry: ['persistSettings'],
|
invoke: {
|
||||||
always: 'idle',
|
src: 'persistSettings',
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: () => {
|
||||||
|
console.error('Error persisting settings')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: ({ context, event }) => {
|
||||||
|
return {
|
||||||
|
doNotPersist: event.doNotPersist ?? false,
|
||||||
|
context,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
loadingUser: {
|
||||||
|
invoke: {
|
||||||
|
src: 'loadUserSettings',
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: 'setAllSettings',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: ({ event }) => {
|
||||||
|
console.error('Error loading user settings', event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loadingProject: {
|
||||||
|
entry: [
|
||||||
|
assign({
|
||||||
|
currentProject: ({ event }) =>
|
||||||
|
event.type === 'load.project' ? event.project : undefined,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
invoke: {
|
||||||
|
src: 'loadProjectSettings',
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: [
|
||||||
|
'setAllSettings',
|
||||||
|
'setThemeColor',
|
||||||
|
'Execute AST',
|
||||||
|
sendTo('registerCommands', { type: 'update' }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onError: 'idle',
|
||||||
|
input: ({ event }) => {
|
||||||
|
return {
|
||||||
|
project: event.type === 'load.project' ? event.project : undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -12,11 +12,9 @@ import {
|
|||||||
getSortFunction,
|
getSortFunction,
|
||||||
getSortIcon,
|
getSortIcon,
|
||||||
} from '../lib/sorting'
|
} from '../lib/sorting'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
|
||||||
import { LowerRightControls } from 'components/LowerRightControls'
|
import { LowerRightControls } from 'components/LowerRightControls'
|
||||||
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
@ -26,6 +24,7 @@ import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
|||||||
import { useProjectsContext } from 'hooks/useProjectsContext'
|
import { useProjectsContext } from 'hooks/useProjectsContext'
|
||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
// This route only opens in the desktop context for now,
|
// This route only opens in the desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||||
@ -46,11 +45,8 @@ const Home = () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {
|
const settings = useSettings()
|
||||||
settings: { context: settings },
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
|
|
||||||
// Cancel all KCL executions while on the home page
|
// Cancel all KCL executions while on the home page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,26 +1,19 @@
|
|||||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import {
|
import {
|
||||||
CameraSystem,
|
CameraSystem,
|
||||||
cameraMouseDragGuards,
|
cameraMouseDragGuards,
|
||||||
cameraSystems,
|
cameraSystems,
|
||||||
} from 'lib/cameraControls'
|
} from 'lib/cameraControls'
|
||||||
import { SettingsSection } from 'components/Settings/SettingsSection'
|
import { SettingsSection } from 'components/Settings/SettingsSection'
|
||||||
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
export default function Units() {
|
export default function Units() {
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.STREAMING)
|
const next = useNextClick(onboardingPaths.STREAMING)
|
||||||
const {
|
const {
|
||||||
settings: {
|
modeling: { mouseControls },
|
||||||
send,
|
} = useSettings()
|
||||||
state: {
|
|
||||||
context: {
|
|
||||||
modeling: { mouseControls },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none">
|
<div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none">
|
||||||
@ -40,7 +33,7 @@ export default function Units() {
|
|||||||
className="block w-full px-3 py-1 bg-transparent border border-chalkboard-30"
|
className="block w-full px-3 py-1 bg-transparent border border-chalkboard-30"
|
||||||
value={mouseControls.current}
|
value={mouseControls.current}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
send({
|
settingsActor.send({
|
||||||
type: 'set.modeling.mouseControls',
|
type: 'set.modeling.mouseControls',
|
||||||
data: {
|
data: {
|
||||||
level: 'user',
|
level: 'user',
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { OnboardingButtons, useDemoCode } from '.'
|
import { OnboardingButtons, useDemoCode } from '.'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
import { bracket } from 'lib/exampleKcl'
|
import { bracket } from 'lib/exampleKcl'
|
||||||
import { createAndOpenNewTutorialProject } from 'lib/desktopFS'
|
import { createAndOpenNewTutorialProject } from 'lib/desktopFS'
|
||||||
@ -14,6 +13,7 @@ import { PATHS } from 'lib/paths'
|
|||||||
import { useFileContext } from 'hooks/useFileContext'
|
import { useFileContext } from 'hooks/useFileContext'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show either a welcome screen or a warning screen
|
* Show either a welcome screen or a warning screen
|
||||||
@ -120,14 +120,8 @@ function OnboardingIntroductionInner() {
|
|||||||
useDemoCode()
|
useDemoCode()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
settings: {
|
app: { theme },
|
||||||
state: {
|
} = useSettings()
|
||||||
context: {
|
|
||||||
app: { theme },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
const getLogoTheme = () =>
|
const getLogoTheme = () =>
|
||||||
theme.current === Themes.Light ||
|
theme.current === Themes.Light ||
|
||||||
(theme.current === Themes.System && getSystemTheme() === Themes.Light)
|
(theme.current === Themes.System && getSystemTheme() === Themes.Light)
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
import { OnboardingButtons, useDemoCode } from '.'
|
import { OnboardingButtons, useDemoCode } from '.'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { bracketThicknessCalculationLine } from 'lib/exampleKcl'
|
import { bracketThicknessCalculationLine } from 'lib/exampleKcl'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
export default function OnboardingParametricModeling() {
|
export default function OnboardingParametricModeling() {
|
||||||
useDemoCode()
|
useDemoCode()
|
||||||
const {
|
const {
|
||||||
settings: {
|
app: {
|
||||||
context: {
|
theme: { current: theme },
|
||||||
app: {
|
|
||||||
theme: { current: theme },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettings()
|
||||||
const getImageTheme = () =>
|
const getImageTheme = () =>
|
||||||
theme === Themes.Light ||
|
theme === Themes.Light ||
|
||||||
(theme === Themes.System && getSystemTheme() === Themes.Light)
|
(theme === Themes.System && getSystemTheme() === Themes.Light)
|
||||||
|
@ -4,19 +4,14 @@ import { ActionButton } from 'components/ActionButton'
|
|||||||
import { SettingsSection } from 'components/Settings/SettingsSection'
|
import { SettingsSection } from 'components/Settings/SettingsSection'
|
||||||
import { useDismiss, useNextClick } from '.'
|
import { useDismiss, useNextClick } from '.'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
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 {
|
const {
|
||||||
settings: {
|
modeling: { defaultUnit },
|
||||||
send,
|
} = useSettings()
|
||||||
context: {
|
|
||||||
modeling: { defaultUnit },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50">
|
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50">
|
||||||
@ -31,7 +26,7 @@ export default function Units() {
|
|||||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||||
value={defaultUnit.user}
|
value={defaultUnit.user}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
send({
|
settingsActor.send({
|
||||||
type: 'set.modeling.defaultUnit',
|
type: 'set.modeling.defaultUnit',
|
||||||
data: {
|
data: {
|
||||||
level: 'user',
|
level: 'user',
|
||||||
|
@ -5,7 +5,6 @@ import Camera from './Camera'
|
|||||||
import Sketching from './Sketching'
|
import Sketching from './Sketching'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
|
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import Streaming from './Streaming'
|
import Streaming from './Streaming'
|
||||||
import CodeEditor from './CodeEditor'
|
import CodeEditor from './CodeEditor'
|
||||||
import ParametricModeling from './ParametricModeling'
|
import ParametricModeling from './ParametricModeling'
|
||||||
@ -26,9 +25,10 @@ import { reportRejection } from 'lib/trap'
|
|||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
|
||||||
|
|
||||||
export const kbdClasses =
|
export const kbdClasses =
|
||||||
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
||||||
@ -112,25 +112,24 @@ export function useDemoCode() {
|
|||||||
|
|
||||||
export function useNextClick(newStatus: string) {
|
export function useNextClick(newStatus: string) {
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
const {
|
|
||||||
settings: { send },
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
send({
|
settingsActor.send({
|
||||||
type: 'set.app.onboardingStatus',
|
type: 'set.app.onboardingStatus',
|
||||||
data: { level: 'user', value: newStatus },
|
data: { level: 'user', value: newStatus },
|
||||||
})
|
})
|
||||||
navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus)
|
navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus)
|
||||||
}, [filePath, newStatus, send, navigate])
|
}, [filePath, newStatus, settingsActor.send, navigate])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDismiss() {
|
export function useDismiss() {
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
const {
|
const settings = useSettings()
|
||||||
settings: { state, send },
|
const send = settingsActor.send
|
||||||
} = useSettingsAuthContext()
|
const isSettingsActorIdle = useSelector(settingsActor, (s) =>
|
||||||
|
s.matches('idle')
|
||||||
|
)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const settingsCallback = useCallback(() => {
|
const settingsCallback = useCallback(() => {
|
||||||
@ -146,12 +145,17 @@ export function useDismiss() {
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
state.context.app.onboardingStatus.user === 'dismissed' &&
|
settings.app.onboardingStatus.current === 'dismissed' &&
|
||||||
state.matches('idle')
|
isSettingsActorIdle
|
||||||
) {
|
) {
|
||||||
navigate(filePath)
|
navigate(filePath)
|
||||||
}
|
}
|
||||||
}, [filePath, navigate, state])
|
}, [
|
||||||
|
filePath,
|
||||||
|
navigate,
|
||||||
|
isSettingsActorIdle,
|
||||||
|
settings.app.onboardingStatus.current,
|
||||||
|
])
|
||||||
|
|
||||||
return settingsCallback
|
return settingsCallback
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { isDesktop } from '../lib/isDesktop'
|
|||||||
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 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { CSSProperties, useCallback, useState } from 'react'
|
import { CSSProperties, useCallback, useState } from 'react'
|
||||||
import { Logo } from 'components/Logo'
|
import { Logo } from 'components/Logo'
|
||||||
@ -15,6 +14,7 @@ import { toSync } from 'lib/utils'
|
|||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { authActor } from 'machines/appMachine'
|
import { authActor } from 'machines/appMachine'
|
||||||
|
import { useSettings } from 'machines/appMachine'
|
||||||
|
|
||||||
const subtleBorder =
|
const subtleBorder =
|
||||||
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
|
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
|
||||||
@ -23,14 +23,8 @@ const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:t
|
|||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
const [userCode, setUserCode] = useState('')
|
const [userCode, setUserCode] = useState('')
|
||||||
const {
|
const {
|
||||||
settings: {
|
app: { theme },
|
||||||
state: {
|
} = useSettings()
|
||||||
context: {
|
|
||||||
app: { theme },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
const signInUrl = `${VITE_KC_SITE_BASE_URL}${
|
const signInUrl = `${VITE_KC_SITE_BASE_URL}${
|
||||||
PATHS.SIGN_IN
|
PATHS.SIGN_IN
|
||||||
}?callbackUrl=${encodeURIComponent(
|
}?callbackUrl=${encodeURIComponent(
|
||||||
|