Compare commits
61 Commits
nested_dir
...
franknoiro
Author | SHA1 | Date | |
---|---|---|---|
7255dea879 | |||
700c98f07f | |||
45a2783ed0 | |||
ab9c736d3c | |||
0dc2216848 | |||
9ea5be90f4 | |||
f2ef6e968b | |||
6cb90195bf | |||
4b537cff24 | |||
8112db21bb | |||
b6d0ad0a9b | |||
81d6c4c1b3 | |||
4e9bdb5b41 | |||
3d30282bc6 | |||
a9e4935619 | |||
f7068e51d3 | |||
3c61d79a65 | |||
7fc61110f6 | |||
e330059e99 | |||
38b447e9da | |||
eeddd4fb07 | |||
5524064902 | |||
fbf94aaba9 | |||
e77b42f308 | |||
e344e4a063 | |||
fb3fcfeef3 | |||
ca466275aa | |||
3bc1547a03 | |||
22251ea353 | |||
6f2e227ea7 | |||
a5fd0150b8 | |||
d210feeebe | |||
5c34fdd619 | |||
4d65a4618e | |||
80af10a988 | |||
01fc2c43c2 | |||
8402975e80 | |||
657883e09a | |||
c68718d835 | |||
0a2e668ee4 | |||
5ac504d000 | |||
60354af367 | |||
9978ad6e23 | |||
c9587fda07 | |||
fcffc72655 | |||
216af5a0ca | |||
af0c591639 | |||
15d676a2c3 | |||
b5ff97c99b | |||
a3b8b1c859 | |||
ba2570d2cd | |||
b3e1326921 | |||
8f3687106e | |||
562f8337bd | |||
8490b3815d | |||
0236926cc8 | |||
5d0530257c | |||
9d673d1903 | |||
983d6160d1 | |||
7c1f3bc484 | |||
4c6ad6d0ca |
@ -3,14 +3,13 @@
|
|||||||
> dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx
|
> dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx
|
||||||
|
|
||||||
• Circular Dependencies
|
• Circular Dependencies
|
||||||
01) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/modifyAst/addEdgeTreatment.ts
|
1) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/modifyAst/addEdgeTreatment.ts
|
||||||
02) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
|
2) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
|
||||||
03) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
|
3) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
|
||||||
04) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
4) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
||||||
05) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts -> src/machines/appMachine.ts -> src/machines/engineStreamMachine.ts
|
5) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
||||||
06) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts -> src/machines/appMachine.ts -> src/machines/settingsMachine.ts
|
6) src/lib/singletons.ts -> src/lang/codeManager.ts
|
||||||
07) src/machines/appMachine.ts -> src/machines/settingsMachine.ts -> src/machines/commandBarMachine.ts -> src/lib/commandBarConfigs/authCommandConfig.ts
|
7) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
|
||||||
08) src/lib/singletons.ts -> src/lang/codeManager.ts
|
8) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/machines/commandBarMachine.ts -> src/lib/commandBarConfigs/authCommandConfig.ts
|
||||||
09) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
|
9) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts
|
||||||
10) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts
|
10) src/routes/Onboarding/index.tsx -> src/routes/Onboarding/Camera.tsx -> src/routes/Onboarding/utils.tsx
|
||||||
11) src/routes/Onboarding/index.tsx -> src/routes/Onboarding/Camera.tsx -> src/routes/Onboarding/utils.tsx
|
|
||||||
|
@ -114,6 +114,7 @@
|
|||||||
"circular-deps": "dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx",
|
"circular-deps": "dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx",
|
||||||
"circular-deps:overwrite": "npm run circular-deps | sed '$d' | grep -v '^npm run' > known-circular.txt",
|
"circular-deps:overwrite": "npm run circular-deps | sed '$d' | grep -v '^npm run' > known-circular.txt",
|
||||||
"circular-deps:diff": "./scripts/diff-circular-deps.sh",
|
"circular-deps:diff": "./scripts/diff-circular-deps.sh",
|
||||||
|
"circular-deps:diff:nodejs": "npm run circular-deps:diff || node ./scripts/diff.js",
|
||||||
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||||
"files:set-notes": "./scripts/set-files-notes.sh",
|
"files:set-notes": "./scripts/set-files-notes.sh",
|
||||||
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
|
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
|
||||||
|
59
scripts/diff.js
Normal file
59
scripts/diff.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const latestRun = fs.readFileSync('/tmp/circular-deps.txt','utf-8')
|
||||||
|
const knownCircular = fs.readFileSync('./known-circular.txt','utf-8')
|
||||||
|
|
||||||
|
function parseLine (line) {
|
||||||
|
let num = null
|
||||||
|
let depPath = null
|
||||||
|
const res = line.split(")",2)
|
||||||
|
if (res.length === 2) {
|
||||||
|
// should be a dep line
|
||||||
|
num = parseInt(res[0])
|
||||||
|
depPath = res[1]
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
num,
|
||||||
|
depPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDependencyHash (file) {
|
||||||
|
const deps = {}
|
||||||
|
file.split("\n").forEach((line)=>{
|
||||||
|
const {num, depPath} = parseLine(line)
|
||||||
|
if (depPath && !isNaN(num)) {
|
||||||
|
deps[depPath] = 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestRunDepHash = makeDependencyHash(latestRun)
|
||||||
|
const knownDepHash = makeDependencyHash(knownCircular)
|
||||||
|
|
||||||
|
const dup1 = JSON.parse(JSON.stringify(latestRunDepHash))
|
||||||
|
const dup2 = JSON.parse(JSON.stringify(knownDepHash))
|
||||||
|
Object.keys(knownDepHash).forEach((key)=>{
|
||||||
|
delete dup1[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.keys(latestRunDepHash).forEach((key)=>{
|
||||||
|
delete dup2[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(" ")
|
||||||
|
console.log("diff.js - line item diff")
|
||||||
|
console.log(" ")
|
||||||
|
console.log("Added(+)")
|
||||||
|
Object.keys(dup1).forEach((dep, index)=>{
|
||||||
|
console.log(`${index+1}) ${dep}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(" ")
|
||||||
|
console.log("Removed(-)")
|
||||||
|
if (Object.keys(dup2).length === 0) {
|
||||||
|
console.log("None")
|
||||||
|
}
|
||||||
|
Object.keys(dup2).forEach((dep, index)=>{
|
||||||
|
console.log(`${index+1}) ${dep}`)
|
||||||
|
})
|
@ -35,11 +35,7 @@ import {
|
|||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { maybeWriteToDisk } from '@src/lib/telemetry'
|
import { maybeWriteToDisk } from '@src/lib/telemetry'
|
||||||
import { type IndexLoaderData } from '@src/lib/types'
|
import { type IndexLoaderData } from '@src/lib/types'
|
||||||
import {
|
import { engineStreamActor, useSettings, useToken } from '@src/lib/singletons'
|
||||||
engineStreamActor,
|
|
||||||
useSettings,
|
|
||||||
useToken,
|
|
||||||
} from '@src/machines/appMachine'
|
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
|
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Loading from '@src/components/Loading'
|
import Loading from '@src/components/Loading'
|
||||||
import { useAuthState } from '@src/machines/appMachine'
|
import { useAuthState } from '@src/lib/singletons'
|
||||||
|
|
||||||
// Wrapper around protected routes, used in src/Router.tsx
|
// Wrapper around protected routes, used in src/Router.tsx
|
||||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||||
|
35
src/Root.tsx
Normal file
35
src/Root.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { AppStateProvider } from '@src/AppState'
|
||||||
|
import LspProvider from '@src/components/LspProvider'
|
||||||
|
import { MachineManagerProvider } from '@src/components/MachineManagerProvider'
|
||||||
|
import { OpenInDesktopAppHandler } from '@src/components/OpenInDesktopAppHandler'
|
||||||
|
import { SystemIOMachineLogicListenerDesktop } from '@src/components/Providers/SystemIOProviderDesktop'
|
||||||
|
import { SystemIOMachineLogicListenerWeb } from '@src/components/Providers/SystemIOProviderWeb'
|
||||||
|
import { RouteProvider } from '@src/components/RouteProvider'
|
||||||
|
import { KclContextProvider } from '@src/lang/KclProvider'
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
|
// Root component will live for the entire applications runtime
|
||||||
|
function RootLayout() {
|
||||||
|
return (
|
||||||
|
<OpenInDesktopAppHandler>
|
||||||
|
<RouteProvider>
|
||||||
|
<LspProvider>
|
||||||
|
<KclContextProvider>
|
||||||
|
<AppStateProvider>
|
||||||
|
<MachineManagerProvider>
|
||||||
|
{isDesktop() ? (
|
||||||
|
<SystemIOMachineLogicListenerDesktop />
|
||||||
|
) : (
|
||||||
|
<SystemIOMachineLogicListenerWeb />
|
||||||
|
)}
|
||||||
|
<Outlet />
|
||||||
|
</MachineManagerProvider>
|
||||||
|
</AppStateProvider>
|
||||||
|
</KclContextProvider>
|
||||||
|
</LspProvider>
|
||||||
|
</RouteProvider>
|
||||||
|
</OpenInDesktopAppHandler>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RootLayout
|
@ -9,22 +9,15 @@ import {
|
|||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
|
|
||||||
import { App } from '@src/App'
|
import { App } from '@src/App'
|
||||||
import { AppStateProvider } from '@src/AppState'
|
|
||||||
import { Auth } from '@src/Auth'
|
import { Auth } from '@src/Auth'
|
||||||
import { CommandBar } from '@src/components/CommandBar/CommandBar'
|
import { CommandBar } from '@src/components/CommandBar/CommandBar'
|
||||||
import DownloadAppBanner from '@src/components/DownloadAppBanner'
|
import DownloadAppBanner from '@src/components/DownloadAppBanner'
|
||||||
import { ErrorPage } from '@src/components/ErrorPage'
|
import { ErrorPage } from '@src/components/ErrorPage'
|
||||||
import FileMachineProvider from '@src/components/FileMachineProvider'
|
import FileMachineProvider from '@src/components/FileMachineProvider'
|
||||||
import LspProvider from '@src/components/LspProvider'
|
|
||||||
import { MachineManagerProvider } from '@src/components/MachineManagerProvider'
|
|
||||||
import ModelingMachineProvider from '@src/components/ModelingMachineProvider'
|
import ModelingMachineProvider from '@src/components/ModelingMachineProvider'
|
||||||
import { OpenInDesktopAppHandler } from '@src/components/OpenInDesktopAppHandler'
|
|
||||||
import { ProjectsContextProvider } from '@src/components/ProjectsContextProvider'
|
|
||||||
import { RouteProvider } from '@src/components/RouteProvider'
|
|
||||||
import { WasmErrBanner } from '@src/components/WasmErrBanner'
|
import { WasmErrBanner } from '@src/components/WasmErrBanner'
|
||||||
import { NetworkContext } from '@src/hooks/useNetworkContext'
|
import { NetworkContext } from '@src/hooks/useNetworkContext'
|
||||||
import { useNetworkStatus } from '@src/hooks/useNetworkStatus'
|
import { useNetworkStatus } from '@src/hooks/useNetworkStatus'
|
||||||
import { KclContextProvider } from '@src/lang/KclProvider'
|
|
||||||
import { coreDump } from '@src/lang/wasm'
|
import { coreDump } from '@src/lang/wasm'
|
||||||
import {
|
import {
|
||||||
ASK_TO_OPEN_QUERY_PARAM,
|
ASK_TO_OPEN_QUERY_PARAM,
|
||||||
@ -42,7 +35,8 @@ import {
|
|||||||
rustContext,
|
rustContext,
|
||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { useToken } from '@src/machines/appMachine'
|
import { useToken } from '@src/lib/singletons'
|
||||||
|
import RootLayout from '@src/Root'
|
||||||
import Home from '@src/routes/Home'
|
import Home from '@src/routes/Home'
|
||||||
import Onboarding, { onboardingRoutes } from '@src/routes/Onboarding'
|
import Onboarding, { onboardingRoutes } from '@src/routes/Onboarding'
|
||||||
import { Settings } from '@src/routes/Settings'
|
import { Settings } from '@src/routes/Settings'
|
||||||
@ -54,27 +48,13 @@ const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
|||||||
const router = createRouter([
|
const router = createRouter([
|
||||||
{
|
{
|
||||||
id: PATHS.INDEX,
|
id: PATHS.INDEX,
|
||||||
element: (
|
element: <RootLayout />,
|
||||||
<OpenInDesktopAppHandler>
|
// Gotcha: declaring errorElement on the root will unmount the element causing our forever React components to unmount.
|
||||||
<RouteProvider>
|
// Leave errorElement on the child components, this allows for the entire react context on error pages as well.
|
||||||
<LspProvider>
|
|
||||||
<ProjectsContextProvider>
|
|
||||||
<KclContextProvider>
|
|
||||||
<AppStateProvider>
|
|
||||||
<MachineManagerProvider>
|
|
||||||
<Outlet />
|
|
||||||
</MachineManagerProvider>
|
|
||||||
</AppStateProvider>
|
|
||||||
</KclContextProvider>
|
|
||||||
</ProjectsContextProvider>
|
|
||||||
</LspProvider>
|
|
||||||
</RouteProvider>
|
|
||||||
</OpenInDesktopAppHandler>
|
|
||||||
),
|
|
||||||
errorElement: <ErrorPage />,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: PATHS.INDEX,
|
path: PATHS.INDEX,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
loader: async ({ request }) => {
|
loader: async ({ request }) => {
|
||||||
const onDesktop = isDesktop()
|
const onDesktop = isDesktop()
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url)
|
||||||
@ -95,6 +75,7 @@ const router = createRouter([
|
|||||||
loader: fileLoader,
|
loader: fileLoader,
|
||||||
id: PATHS.FILE,
|
id: PATHS.FILE,
|
||||||
path: PATHS.FILE + '/:id',
|
path: PATHS.FILE + '/:id',
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
element: (
|
element: (
|
||||||
<Auth>
|
<Auth>
|
||||||
<FileMachineProvider>
|
<FileMachineProvider>
|
||||||
@ -141,6 +122,7 @@ const router = createRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: PATHS.HOME,
|
path: PATHS.HOME,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
element: (
|
element: (
|
||||||
<Auth>
|
<Auth>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@ -169,6 +151,7 @@ const router = createRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: PATHS.SIGN_IN,
|
path: PATHS.SIGN_IN,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
element: <SignIn />,
|
element: <SignIn />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -40,7 +40,7 @@ import {
|
|||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { err, reportRejection, trap } from '@src/lib/trap'
|
import { err, reportRejection, trap } from '@src/lib/trap'
|
||||||
import { throttle, toSync } from '@src/lib/utils'
|
import { throttle, toSync } from '@src/lib/utils'
|
||||||
import type { useSettings } from '@src/machines/appMachine'
|
import type { useSettings } from '@src/lib/singletons'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import type { SegmentOverlay } from '@src/machines/modelingMachine'
|
import type { SegmentOverlay } from '@src/machines/modelingMachine'
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { RefreshButton } from '@src/components/RefreshButton'
|
|||||||
import UserSidebarMenu from '@src/components/UserSidebarMenu'
|
import UserSidebarMenu from '@src/components/UserSidebarMenu'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { type IndexLoaderData } from '@src/lib/types'
|
import { type IndexLoaderData } from '@src/lib/types'
|
||||||
import { useUser } from '@src/machines/appMachine'
|
import { useUser } from '@src/lib/singletons'
|
||||||
|
|
||||||
import styles from './AppHeader.module.css'
|
import styles from './AppHeader.module.css'
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Switch } from '@headlessui/react'
|
import { Switch } from '@headlessui/react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { settingsActor, useSettings } from '@src/machines/appMachine'
|
import { settingsActor, useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
export function CameraProjectionToggle() {
|
export function CameraProjectionToggle() {
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
@ -29,7 +29,7 @@ import { err } from '@src/lib/trap'
|
|||||||
import { useCalculateKclExpression } from '@src/lib/useCalculateKclExpression'
|
import { useCalculateKclExpression } from '@src/lib/useCalculateKclExpression'
|
||||||
import { roundOff } from '@src/lib/utils'
|
import { roundOff } from '@src/lib/utils'
|
||||||
import { varMentions } from '@src/lib/varCompletionExtension'
|
import { varMentions } from '@src/lib/varCompletionExtension'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import {
|
import {
|
||||||
commandBarActor,
|
commandBarActor,
|
||||||
useCommandBarState,
|
useCommandBarState,
|
||||||
|
@ -3,7 +3,7 @@ import { useState } from 'react'
|
|||||||
|
|
||||||
import { ActionButton } from '@src/components/ActionButton'
|
import { ActionButton } from '@src/components/ActionButton'
|
||||||
import { CREATE_FILE_URL_PARAM } from '@src/lib/constants'
|
import { CREATE_FILE_URL_PARAM } from '@src/lib/constants'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
const DownloadAppBanner = () => {
|
const DownloadAppBanner = () => {
|
||||||
|
@ -18,7 +18,7 @@ import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from '@src/lib/timings'
|
|||||||
import { err, reportRejection, trap } from '@src/lib/trap'
|
import { err, reportRejection, trap } from '@src/lib/trap'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
import { uuidv4 } from '@src/lib/utils'
|
import { uuidv4 } from '@src/lib/utils'
|
||||||
import { engineStreamActor, useSettings } from '@src/machines/appMachine'
|
import { engineStreamActor, useSettings } from '@src/lib/singletons'
|
||||||
import { useCommandBarState } from '@src/machines/commandBarMachine'
|
import { useCommandBarState } from '@src/machines/commandBarMachine'
|
||||||
import {
|
import {
|
||||||
EngineStreamState,
|
EngineStreamState,
|
||||||
|
@ -33,7 +33,7 @@ import { markOnce } from '@src/lib/performance'
|
|||||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
import { codeManager, kclManager } from '@src/lib/singletons'
|
||||||
import { err, reportRejection } from '@src/lib/trap'
|
import { err, reportRejection } from '@src/lib/trap'
|
||||||
import { type IndexLoaderData } from '@src/lib/types'
|
import { type IndexLoaderData } from '@src/lib/types'
|
||||||
import { useSettings, useToken } from '@src/machines/appMachine'
|
import { useSettings, useToken } from '@src/lib/singletons'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import { fileMachine } from '@src/machines/fileMachine'
|
import { fileMachine } from '@src/machines/fileMachine'
|
||||||
import { modelingMenuCallbackMostActions } from '@src/menu/register'
|
import { modelingMenuCallbackMostActions } from '@src/menu/register'
|
||||||
|
@ -27,7 +27,7 @@ import { useModelingContext } from '@src/hooks/useModelingContext'
|
|||||||
import { AxisNames } from '@src/lib/constants'
|
import { AxisNames } from '@src/lib/constants'
|
||||||
import { sceneInfra } from '@src/lib/singletons'
|
import { sceneInfra } from '@src/lib/singletons'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
|
@ -10,7 +10,7 @@ import { createAndOpenNewTutorialProject } from '@src/lib/desktopFS'
|
|||||||
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { settingsActor } from '@src/machines/appMachine'
|
import { settingsActor } from '@src/lib/singletons'
|
||||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
import type { WebContentSendPayload } from '@src/menu/channels'
|
||||||
|
|
||||||
const HelpMenuDivider = () => (
|
const HelpMenuDivider = () => (
|
||||||
|
@ -27,7 +27,7 @@ import { PATHS } from '@src/lib/paths'
|
|||||||
import type { FileEntry } from '@src/lib/project'
|
import type { FileEntry } from '@src/lib/project'
|
||||||
import { codeManager } from '@src/lib/singletons'
|
import { codeManager } from '@src/lib/singletons'
|
||||||
import { err } from '@src/lib/trap'
|
import { err } from '@src/lib/trap'
|
||||||
import { useToken } from '@src/machines/appMachine'
|
import { useToken } from '@src/lib/singletons'
|
||||||
|
|
||||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||||
return []
|
return []
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { engineStreamActor } from '@src/machines/appMachine'
|
import { engineStreamActor } from '@src/lib/singletons'
|
||||||
import { EngineStreamState } from '@src/machines/engineStreamMachine'
|
import { EngineStreamState } from '@src/machines/engineStreamMachine'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ import { submitAndAwaitTextToKcl } from '@src/lib/textToCad'
|
|||||||
import { err, reject, reportRejection, trap } from '@src/lib/trap'
|
import { err, reject, reportRejection, trap } from '@src/lib/trap'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
import { platform, uuidv4 } from '@src/lib/utils'
|
import { platform, uuidv4 } from '@src/lib/utils'
|
||||||
import { useSettings, useToken } from '@src/machines/appMachine'
|
import { useSettings, useToken } from '@src/lib/singletons'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import { kclEditorActor } from '@src/machines/kclEditorMachine'
|
import { kclEditorActor } from '@src/machines/kclEditorMachine'
|
||||||
import {
|
import {
|
||||||
|
@ -5,7 +5,7 @@ import { ActionButton } from '@src/components/ActionButton'
|
|||||||
import { ActionIcon } from '@src/components/ActionIcon'
|
import { ActionIcon } from '@src/components/ActionIcon'
|
||||||
import type { CustomIconName } from '@src/components/CustomIcon'
|
import type { CustomIconName } from '@src/components/CustomIcon'
|
||||||
import Tooltip from '@src/components/Tooltip'
|
import Tooltip from '@src/components/Tooltip'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
|
||||||
import styles from './ModelingPane.module.css'
|
import styles from './ModelingPane.module.css'
|
||||||
|
@ -47,7 +47,7 @@ import { codeManagerHistoryCompartment } from '@src/lang/codeManager'
|
|||||||
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
||||||
import { Themes, getSystemTheme } from '@src/lib/theme'
|
import { Themes, getSystemTheme } from '@src/lib/theme'
|
||||||
import { onMouseDragMakeANewNumber, onMouseDragRegex } from '@src/lib/utils'
|
import { onMouseDragMakeANewNumber, onMouseDragRegex } from '@src/lib/utils'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import {
|
import {
|
||||||
editorIsMountedSelector,
|
editorIsMountedSelector,
|
||||||
kclEditorActor,
|
kclEditorActor,
|
||||||
|
@ -22,7 +22,7 @@ import { useKclContext } from '@src/lang/KclProvider'
|
|||||||
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
|
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
|
||||||
import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
|
import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
kclManager,
|
kclManager,
|
||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { type IndexLoaderData } from '@src/lib/types'
|
import { type IndexLoaderData } from '@src/lib/types'
|
||||||
import { useToken } from '@src/machines/appMachine'
|
import { useToken } from '@src/lib/singletons'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
|
@ -1,492 +0,0 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
|
||||||
import { createContext, useCallback, useEffect, useState } from 'react'
|
|
||||||
import toast from 'react-hot-toast'
|
|
||||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
|
||||||
import type { Actor, AnyStateMachine, Prop, StateFrom } from 'xstate'
|
|
||||||
import { fromPromise } from 'xstate'
|
|
||||||
|
|
||||||
import { useLspContext } from '@src/components/LspProvider'
|
|
||||||
import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher'
|
|
||||||
import { useProjectsLoader } from '@src/hooks/useProjectsLoader'
|
|
||||||
import useStateMachineCommands from '@src/hooks/useStateMachineCommands'
|
|
||||||
import { newKclFile } from '@src/lang/project'
|
|
||||||
import { projectsCommandBarConfig } from '@src/lib/commandBarConfigs/projectsCommandConfig'
|
|
||||||
import {
|
|
||||||
CREATE_FILE_URL_PARAM,
|
|
||||||
FILE_EXT,
|
|
||||||
PROJECT_ENTRYPOINT,
|
|
||||||
} from '@src/lib/constants'
|
|
||||||
import {
|
|
||||||
createNewProjectDirectory,
|
|
||||||
listProjects,
|
|
||||||
renameProjectDirectory,
|
|
||||||
} from '@src/lib/desktop'
|
|
||||||
import {
|
|
||||||
doesProjectNameNeedInterpolated,
|
|
||||||
getNextFileName,
|
|
||||||
getNextProjectIndex,
|
|
||||||
getUniqueProjectName,
|
|
||||||
interpolateProjectNameWithIndex,
|
|
||||||
} from '@src/lib/desktopFS'
|
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
|
||||||
import { PATHS } from '@src/lib/paths'
|
|
||||||
import type { Project } from '@src/lib/project'
|
|
||||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
|
||||||
import { err } from '@src/lib/trap'
|
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
|
||||||
import { projectsMachine } from '@src/machines/projectsMachine'
|
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
|
||||||
state?: StateFrom<T>
|
|
||||||
send: Prop<Actor<T>, 'send'>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProjectsMachineContext = createContext(
|
|
||||||
{} as MachineContext<typeof projectsMachine>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Watches the project directory and provides project management-related commands,
|
|
||||||
* like "Create project", "Open project", "Delete project", etc.
|
|
||||||
*
|
|
||||||
* If in the future we implement full-fledge project management in the web version,
|
|
||||||
* we can unify these components but for now, we need this to be only for the desktop version.
|
|
||||||
*/
|
|
||||||
export const ProjectsContextProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
return isDesktop() ? (
|
|
||||||
<ProjectsContextDesktop>{children}</ProjectsContextDesktop>
|
|
||||||
) : (
|
|
||||||
<ProjectsContextWeb>{children}</ProjectsContextWeb>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need some of the functionality of the ProjectsContextProvider in the web version
|
|
||||||
* but we can't perform file system operations in the browser,
|
|
||||||
* so most of the behavior of this machine is stubbed out.
|
|
||||||
*/
|
|
||||||
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
|
||||||
const clearImportSearchParams = useCallback(() => {
|
|
||||||
// Clear the search parameters related to the "Import file from URL" command
|
|
||||||
// or we'll never be able cancel or submit it.
|
|
||||||
searchParams.delete(CREATE_FILE_URL_PARAM)
|
|
||||||
searchParams.delete('code')
|
|
||||||
searchParams.delete('name')
|
|
||||||
searchParams.delete('units')
|
|
||||||
setSearchParams(searchParams)
|
|
||||||
}, [searchParams, setSearchParams])
|
|
||||||
const settings = useSettings()
|
|
||||||
|
|
||||||
const [state, send, actor] = useMachine(
|
|
||||||
projectsMachine.provide({
|
|
||||||
actions: {
|
|
||||||
navigateToProject: () => {},
|
|
||||||
navigateToProjectIfNeeded: () => {},
|
|
||||||
navigateToFile: () => {},
|
|
||||||
toastSuccess: ({ event }) =>
|
|
||||||
toast.success(
|
|
||||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
|
||||||
('output' in event &&
|
|
||||||
'message' in event.output &&
|
|
||||||
typeof event.output.message === 'string' &&
|
|
||||||
event.output.message) ||
|
|
||||||
''
|
|
||||||
),
|
|
||||||
toastError: ({ event }) =>
|
|
||||||
toast.error(
|
|
||||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
|
||||||
('output' in event &&
|
|
||||||
typeof event.output === 'string' &&
|
|
||||||
event.output) ||
|
|
||||||
''
|
|
||||||
),
|
|
||||||
},
|
|
||||||
actors: {
|
|
||||||
readProjects: fromPromise(async () => [] as Project[]),
|
|
||||||
createProject: fromPromise(async () => ({
|
|
||||||
message: 'not implemented on web',
|
|
||||||
})),
|
|
||||||
renameProject: fromPromise(async () => ({
|
|
||||||
message: 'not implemented on web',
|
|
||||||
oldName: '',
|
|
||||||
newName: '',
|
|
||||||
})),
|
|
||||||
deleteProject: fromPromise(async () => ({
|
|
||||||
message: 'not implemented on web',
|
|
||||||
name: '',
|
|
||||||
})),
|
|
||||||
createFile: fromPromise(async ({ input }) => {
|
|
||||||
// Browser version doesn't navigate, just overwrites the current file
|
|
||||||
clearImportSearchParams()
|
|
||||||
|
|
||||||
const codeToWrite = newKclFile(
|
|
||||||
input.code,
|
|
||||||
settings.modeling.defaultUnit.current
|
|
||||||
)
|
|
||||||
if (err(codeToWrite)) return Promise.reject(codeToWrite)
|
|
||||||
codeManager.updateCodeStateEditor(codeToWrite)
|
|
||||||
await codeManager.writeToFile()
|
|
||||||
await kclManager.executeCode()
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: 'File overwritten successfully',
|
|
||||||
fileName: input.name,
|
|
||||||
projectName: '',
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
input: {
|
|
||||||
projects: [],
|
|
||||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
|
||||||
defaultDirectory: settings.app.projectDirectory.current,
|
|
||||||
hasListedProjects: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// register all project-related command palette commands
|
|
||||||
useStateMachineCommands({
|
|
||||||
machineId: 'projects',
|
|
||||||
send,
|
|
||||||
state,
|
|
||||||
commandBarConfig: projectsCommandBarConfig,
|
|
||||||
actor,
|
|
||||||
onCancel: clearImportSearchParams,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProjectsMachineContext.Provider
|
|
||||||
value={{
|
|
||||||
state,
|
|
||||||
send,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ProjectsMachineContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectsContextDesktop = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
|
||||||
const clearImportSearchParams = useCallback(() => {
|
|
||||||
// Clear the search parameters related to the "Import file from URL" command
|
|
||||||
// or we'll never be able cancel or submit it.
|
|
||||||
searchParams.delete(CREATE_FILE_URL_PARAM)
|
|
||||||
searchParams.delete('code')
|
|
||||||
searchParams.delete('name')
|
|
||||||
searchParams.delete('units')
|
|
||||||
setSearchParams(searchParams)
|
|
||||||
}, [searchParams, setSearchParams])
|
|
||||||
const { onProjectOpen } = useLspContext()
|
|
||||||
const settings = useSettings()
|
|
||||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
|
||||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
|
||||||
projectsLoaderTrigger,
|
|
||||||
])
|
|
||||||
|
|
||||||
const [state, send, actor] = useMachine(
|
|
||||||
projectsMachine.provide({
|
|
||||||
actions: {
|
|
||||||
navigateToProject: ({ context, event }) => {
|
|
||||||
const nameFromEventData =
|
|
||||||
'data' in event &&
|
|
||||||
event.data &&
|
|
||||||
'name' in event.data &&
|
|
||||||
event.data.name
|
|
||||||
const nameFromOutputData =
|
|
||||||
'output' in event &&
|
|
||||||
event.output &&
|
|
||||||
'name' in event.output &&
|
|
||||||
event.output.name
|
|
||||||
|
|
||||||
const name = nameFromEventData || nameFromOutputData
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
let projectPath =
|
|
||||||
context.defaultDirectory + window.electron.path.sep + name
|
|
||||||
onProjectOpen(
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
path: projectPath,
|
|
||||||
},
|
|
||||||
null
|
|
||||||
)
|
|
||||||
commandBarActor.send({ type: 'Close' })
|
|
||||||
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
|
|
||||||
projectPath
|
|
||||||
)}`
|
|
||||||
navigate(newPathName)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigateToProjectIfNeeded: ({ event }) => {
|
|
||||||
if (
|
|
||||||
event.type.startsWith('xstate.done.actor.') &&
|
|
||||||
'output' in event
|
|
||||||
) {
|
|
||||||
const isInAProject = location.pathname.startsWith(PATHS.FILE)
|
|
||||||
const isInDeletedProject =
|
|
||||||
event.type === 'xstate.done.actor.delete-project' &&
|
|
||||||
isInAProject &&
|
|
||||||
decodeURIComponent(location.pathname).includes(event.output.name)
|
|
||||||
if (isInDeletedProject) {
|
|
||||||
navigate(PATHS.HOME)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isInRenamedProject =
|
|
||||||
event.type === 'xstate.done.actor.rename-project' &&
|
|
||||||
isInAProject &&
|
|
||||||
decodeURIComponent(location.pathname).includes(
|
|
||||||
event.output.oldName
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isInRenamedProject) {
|
|
||||||
// TODO: In future, we can navigate to the new project path
|
|
||||||
// directly, but we need to coordinate with
|
|
||||||
// @lf94's useFileSystemWatcher in SettingsAuthProvider.tsx:224
|
|
||||||
// Because it's beating us to the punch and updating the route
|
|
||||||
// const newPathName = location.pathname.replace(
|
|
||||||
// encodeURIComponent(event.output.oldName),
|
|
||||||
// encodeURIComponent(event.output.newName)
|
|
||||||
// )
|
|
||||||
// navigate(newPathName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigateToFile: ({ context, event }) => {
|
|
||||||
if (event.type !== 'xstate.done.actor.create-file') return
|
|
||||||
// For now, the browser version of create-file doesn't need to navigate
|
|
||||||
// since it just overwrites the current file.
|
|
||||||
if (!isDesktop()) return
|
|
||||||
let projectPath = window.electron.join(
|
|
||||||
context.defaultDirectory,
|
|
||||||
event.output.projectName
|
|
||||||
)
|
|
||||||
let filePath = window.electron.join(
|
|
||||||
projectPath,
|
|
||||||
event.output.fileName
|
|
||||||
)
|
|
||||||
onProjectOpen(
|
|
||||||
{
|
|
||||||
name: event.output.projectName,
|
|
||||||
path: projectPath,
|
|
||||||
},
|
|
||||||
null
|
|
||||||
)
|
|
||||||
const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent(
|
|
||||||
filePath
|
|
||||||
)}`
|
|
||||||
navigate(pathToNavigateTo)
|
|
||||||
},
|
|
||||||
toastSuccess: ({ event }) =>
|
|
||||||
toast.success(
|
|
||||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
|
||||||
('output' in event &&
|
|
||||||
'message' in event.output &&
|
|
||||||
typeof event.output.message === 'string' &&
|
|
||||||
event.output.message) ||
|
|
||||||
''
|
|
||||||
),
|
|
||||||
toastError: ({ event }) =>
|
|
||||||
toast.error(
|
|
||||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
|
||||||
('output' in event &&
|
|
||||||
typeof event.output === 'string' &&
|
|
||||||
event.output) ||
|
|
||||||
('error' in event &&
|
|
||||||
event.error instanceof Error &&
|
|
||||||
event.error.message) ||
|
|
||||||
''
|
|
||||||
),
|
|
||||||
},
|
|
||||||
actors: {
|
|
||||||
readProjects: fromPromise(() => {
|
|
||||||
return listProjects()
|
|
||||||
}),
|
|
||||||
createProject: fromPromise(async ({ input }) => {
|
|
||||||
let name = (
|
|
||||||
input && 'name' in input && input.name
|
|
||||||
? input.name
|
|
||||||
: settings.projects.defaultProjectName.current
|
|
||||||
).trim()
|
|
||||||
|
|
||||||
const uniqueName = getUniqueProjectName(name, input.projects)
|
|
||||||
await createNewProjectDirectory(uniqueName)
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: `Successfully created "${uniqueName}"`,
|
|
||||||
name: uniqueName,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
renameProject: fromPromise(async ({ input }) => {
|
|
||||||
const {
|
|
||||||
oldName,
|
|
||||||
newName,
|
|
||||||
defaultProjectName,
|
|
||||||
defaultDirectory,
|
|
||||||
projects,
|
|
||||||
} = input
|
|
||||||
let name = newName ? newName : defaultProjectName
|
|
||||||
if (doesProjectNameNeedInterpolated(name)) {
|
|
||||||
const nextIndex = getNextProjectIndex(name, projects)
|
|
||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toast an error if the project name is taken
|
|
||||||
if (projects.find((p) => p.name === name)) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(`Project with name "${name}" already exists`)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await renameProjectDirectory(
|
|
||||||
window.electron.path.join(defaultDirectory, oldName),
|
|
||||||
name
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
message: `Successfully renamed "${oldName}" to "${name}"`,
|
|
||||||
oldName: oldName,
|
|
||||||
newName: name,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
deleteProject: fromPromise(async ({ input }) => {
|
|
||||||
await window.electron.rm(
|
|
||||||
window.electron.path.join(input.defaultDirectory, input.name),
|
|
||||||
{
|
|
||||||
recursive: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
message: `Successfully deleted "${input.name}"`,
|
|
||||||
name: input.name,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
createFile: fromPromise(async ({ input }) => {
|
|
||||||
let projectName =
|
|
||||||
(input.method === 'newProject' ? input.name : input.projectName) ||
|
|
||||||
settings.projects.defaultProjectName.current
|
|
||||||
let fileName =
|
|
||||||
input.method === 'newProject'
|
|
||||||
? PROJECT_ENTRYPOINT
|
|
||||||
: input.name.endsWith(FILE_EXT)
|
|
||||||
? input.name
|
|
||||||
: input.name + FILE_EXT
|
|
||||||
let message = 'File created successfully'
|
|
||||||
|
|
||||||
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
|
|
||||||
if (needsInterpolated) {
|
|
||||||
const nextIndex = getNextProjectIndex(projectName, input.projects)
|
|
||||||
projectName = interpolateProjectNameWithIndex(
|
|
||||||
projectName,
|
|
||||||
nextIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the project around the file if newProject
|
|
||||||
let fileLoaded = false
|
|
||||||
if (input.method === 'newProject') {
|
|
||||||
await createNewProjectDirectory(projectName, input.code)
|
|
||||||
fileLoaded = true
|
|
||||||
message = `Project "${projectName}" created successfully with link contents`
|
|
||||||
} else {
|
|
||||||
message = `File "${fileName}" created successfully`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the file
|
|
||||||
let baseDir = window.electron.join(
|
|
||||||
settings.app.projectDirectory.current,
|
|
||||||
projectName
|
|
||||||
)
|
|
||||||
const { name, path } = getNextFileName({
|
|
||||||
entryName: fileName,
|
|
||||||
baseDir,
|
|
||||||
})
|
|
||||||
|
|
||||||
fileName = name
|
|
||||||
if (!fileLoaded) {
|
|
||||||
const codeToWrite = newKclFile(
|
|
||||||
input.code,
|
|
||||||
settings.modeling.defaultUnit.current
|
|
||||||
)
|
|
||||||
if (err(codeToWrite)) return Promise.reject(codeToWrite)
|
|
||||||
await window.electron.writeFile(path, codeToWrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Return the project's file name if one was created.
|
|
||||||
return {
|
|
||||||
message,
|
|
||||||
fileName,
|
|
||||||
projectName,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
input: {
|
|
||||||
projects: projectPaths,
|
|
||||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
|
||||||
defaultDirectory: settings.app.projectDirectory.current,
|
|
||||||
hasListedProjects: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
useFileSystemWatcher(
|
|
||||||
async () => {
|
|
||||||
// Gotcha: Chokidar is buggy. It will emit addDir or add on files that did not get created.
|
|
||||||
// This means while the application initialize and Chokidar initializes you cannot tell if
|
|
||||||
// a directory or file is actually created or they are buggy signals. This means you must
|
|
||||||
// ignore all signals during initialization because it is ambiguous. Once those signals settle
|
|
||||||
// you can actually start listening to real signals.
|
|
||||||
// If someone creates folders or files during initialization we ignore those events!
|
|
||||||
if (!actor.getSnapshot().context.hasListedProjects) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return setProjectsLoaderTrigger(projectsLoaderTrigger + 1)
|
|
||||||
},
|
|
||||||
projectsDir ? [projectsDir] : []
|
|
||||||
)
|
|
||||||
|
|
||||||
// Gotcha: Triggers listProjects() on chokidar changes
|
|
||||||
// Gotcha: Load the projects when the projectDirectory changes.
|
|
||||||
const projectDirectory = settings.app.projectDirectory.current
|
|
||||||
useEffect(() => {
|
|
||||||
send({ type: 'Read projects', data: {} })
|
|
||||||
}, [projectPaths, projectDirectory])
|
|
||||||
|
|
||||||
// register all project-related command palette commands
|
|
||||||
useStateMachineCommands({
|
|
||||||
machineId: 'projects',
|
|
||||||
send,
|
|
||||||
state,
|
|
||||||
commandBarConfig: projectsCommandBarConfig,
|
|
||||||
actor,
|
|
||||||
onCancel: clearImportSearchParams,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProjectsMachineContext.Provider
|
|
||||||
value={{
|
|
||||||
state,
|
|
||||||
send,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ProjectsMachineContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
122
src/components/Providers/SystemIOProviderDesktop.tsx
Normal file
122
src/components/Providers/SystemIOProviderDesktop.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher'
|
||||||
|
import { PATHS } from '@src/lib/paths'
|
||||||
|
import { systemIOActor, useSettings } from '@src/lib/singletons'
|
||||||
|
import {
|
||||||
|
useHasListedProjects,
|
||||||
|
useProjectDirectoryPath,
|
||||||
|
useRequestedFileName,
|
||||||
|
useRequestedProjectName,
|
||||||
|
} from '@src/machines/systemIO/hooks'
|
||||||
|
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useEffect, useCallback } from 'react'
|
||||||
|
import { useClearURLParams } from '@src/machines/systemIO/hooks'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { CREATE_FILE_URL_PARAM } from '@src/lib/constants'
|
||||||
|
|
||||||
|
export function SystemIOMachineLogicListenerDesktop() {
|
||||||
|
const requestedProjectName = useRequestedProjectName()
|
||||||
|
const requestedFileName = useRequestedFileName()
|
||||||
|
const projectDirectoryPath = useProjectDirectoryPath()
|
||||||
|
const hasListedProjects = useHasListedProjects()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const settings = useSettings()
|
||||||
|
const clearURLParams = useClearURLParams()
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const clearImportSearchParams = useCallback(() => {
|
||||||
|
// Clear the search parameters related to the "Import file from URL" command
|
||||||
|
// or we'll never be able cancel or submit it.
|
||||||
|
searchParams.delete(CREATE_FILE_URL_PARAM)
|
||||||
|
searchParams.delete('code')
|
||||||
|
searchParams.delete('name')
|
||||||
|
searchParams.delete('units')
|
||||||
|
setSearchParams(searchParams)
|
||||||
|
}, [searchParams, setSearchParams])
|
||||||
|
|
||||||
|
const useGlobalProjectNavigation = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requestedProjectName.name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let projectPathWithoutSpecificKCLFile =
|
||||||
|
projectDirectoryPath +
|
||||||
|
window.electron.path.sep +
|
||||||
|
requestedProjectName.name
|
||||||
|
|
||||||
|
const requestedPath = `${PATHS.FILE}/${encodeURIComponent(
|
||||||
|
projectPathWithoutSpecificKCLFile
|
||||||
|
)}`
|
||||||
|
navigate(requestedPath)
|
||||||
|
}, [requestedProjectName])
|
||||||
|
}
|
||||||
|
|
||||||
|
const useGlobalFileNavigation = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(requestedFileName, 'NEW!')
|
||||||
|
if (!requestedFileName.file || !requestedFileName.project) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const projectPath = window.electron.join(
|
||||||
|
projectDirectoryPath,
|
||||||
|
requestedFileName.project
|
||||||
|
)
|
||||||
|
const filePath = window.electron.join(projectPath, requestedFileName.file)
|
||||||
|
const requestedPath = `${PATHS.FILE}/${encodeURIComponent(filePath)}`
|
||||||
|
navigate(requestedPath)
|
||||||
|
}, [requestedFileName])
|
||||||
|
}
|
||||||
|
|
||||||
|
const useApplicationProjectDirectory = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.setProjectDirectoryPath,
|
||||||
|
data: {
|
||||||
|
requestedProjectDirectoryPath:
|
||||||
|
settings.app.projectDirectory.current || '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [settings.app.projectDirectory.current])
|
||||||
|
}
|
||||||
|
|
||||||
|
const useDefaultProjectName = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.setDefaultProjectFolderName,
|
||||||
|
data: {
|
||||||
|
requestedDefaultProjectFolderName:
|
||||||
|
settings.projects.defaultProjectName.current || '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [settings.projects.defaultProjectName.current])
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWatchingApplicationProjectDirectory = () => {
|
||||||
|
useFileSystemWatcher(
|
||||||
|
async () => {
|
||||||
|
// Gotcha: Chokidar is buggy. It will emit addDir or add on files that did not get created.
|
||||||
|
// This means while the application initialize and Chokidar initializes you cannot tell if
|
||||||
|
// a directory or file is actually created or they are buggy signals. This means you must
|
||||||
|
// ignore all signals during initialization because it is ambiguous. Once those signals settle
|
||||||
|
// you can actually start listening to real signals.
|
||||||
|
// If someone creates folders or files during initialization we ignore those events!
|
||||||
|
if (!hasListedProjects) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.readFoldersFromProjectDirectory,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings.app.projectDirectory.current
|
||||||
|
? [settings.app.projectDirectory.current]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useGlobalProjectNavigation()
|
||||||
|
useGlobalFileNavigation()
|
||||||
|
useApplicationProjectDirectory()
|
||||||
|
useDefaultProjectName()
|
||||||
|
useWatchingApplicationProjectDirectory()
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
29
src/components/Providers/SystemIOProviderWeb.tsx
Normal file
29
src/components/Providers/SystemIOProviderWeb.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect, useCallback } from 'react'
|
||||||
|
import { useClearURLParams } from '@src/machines/systemIO/hooks'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { CREATE_FILE_URL_PARAM } from '@src/lib/constants'
|
||||||
|
|
||||||
|
export function SystemIOMachineLogicListenerWeb() {
|
||||||
|
const clearURLParams = useClearURLParams()
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const clearImportSearchParams = useCallback(() => {
|
||||||
|
// Clear the search parameters related to the "Import file from URL" command
|
||||||
|
// or we'll never be able cancel or submit it.
|
||||||
|
searchParams.delete(CREATE_FILE_URL_PARAM)
|
||||||
|
searchParams.delete('code')
|
||||||
|
searchParams.delete('name')
|
||||||
|
searchParams.delete('units')
|
||||||
|
setSearchParams(searchParams)
|
||||||
|
}, [searchParams, setSearchParams])
|
||||||
|
|
||||||
|
const useClearQueryParams = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (clearURLParams.value) {
|
||||||
|
clearImportSearchParams()
|
||||||
|
}
|
||||||
|
}, [clearURLParams])
|
||||||
|
}
|
||||||
|
|
||||||
|
useClearQueryParams()
|
||||||
|
return null
|
||||||
|
}
|
@ -13,7 +13,7 @@ import {
|
|||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { toSync } from '@src/lib/utils'
|
import { toSync } from '@src/lib/utils'
|
||||||
import { useToken } from '@src/machines/appMachine'
|
import { useToken } from '@src/lib/singletons'
|
||||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
import type { WebContentSendPayload } from '@src/menu/channels'
|
||||||
|
|
||||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||||
|
@ -18,7 +18,7 @@ import { markOnce } from '@src/lib/performance'
|
|||||||
import { loadAndValidateSettings } from '@src/lib/settings/settingsUtils'
|
import { loadAndValidateSettings } from '@src/lib/settings/settingsUtils'
|
||||||
import { trap } from '@src/lib/trap'
|
import { trap } from '@src/lib/trap'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
import { settingsActor, useSettings } from '@src/machines/appMachine'
|
import { settingsActor, useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
export const RouteProviderContext = createContext({})
|
export const RouteProviderContext = createContext({})
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ import {
|
|||||||
} from '@src/lib/settings/settingsUtils'
|
} from '@src/lib/settings/settingsUtils'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { toSync } from '@src/lib/utils'
|
import { toSync } from '@src/lib/utils'
|
||||||
import { settingsActor, useSettings } from '@src/machines/appMachine'
|
import { settingsActor, useSettings } from '@src/lib/singletons'
|
||||||
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from '@src/routes/utils'
|
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from '@src/routes/utils'
|
||||||
import { waitFor } from 'xstate'
|
import { waitFor } from 'xstate'
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import type {
|
|||||||
WildcardSetEvent,
|
WildcardSetEvent,
|
||||||
} from '@src/lib/settings/settingsTypes'
|
} from '@src/lib/settings/settingsTypes'
|
||||||
import { getSettingInputType } from '@src/lib/settings/settingsUtils'
|
import { getSettingInputType } from '@src/lib/settings/settingsUtils'
|
||||||
import { settingsActor, useSettings } from '@src/machines/appMachine'
|
import { settingsActor, useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
interface SettingsFieldInputProps {
|
interface SettingsFieldInputProps {
|
||||||
// We don't need the fancy types here,
|
// We don't need the fancy types here,
|
||||||
|
@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { CustomIcon } from '@src/components/CustomIcon'
|
import { CustomIcon } from '@src/components/CustomIcon'
|
||||||
import { interactionMap } from '@src/lib/settings/initialKeybindings'
|
import { interactionMap } from '@src/lib/settings/initialKeybindings'
|
||||||
import type { SettingsLevel } from '@src/lib/settings/settingsTypes'
|
import type { SettingsLevel } from '@src/lib/settings/settingsTypes'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
type ExtendedSettingsLevel = SettingsLevel | 'keybindings'
|
type ExtendedSettingsLevel = SettingsLevel | 'keybindings'
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import decamelize from 'decamelize'
|
|||||||
import type { Setting } from '@src/lib/settings/initialSettings'
|
import type { Setting } from '@src/lib/settings/initialSettings'
|
||||||
import type { SettingsLevel } from '@src/lib/settings/settingsTypes'
|
import type { SettingsLevel } from '@src/lib/settings/settingsTypes'
|
||||||
import { shouldHideSetting } from '@src/lib/settings/settingsUtils'
|
import { shouldHideSetting } from '@src/lib/settings/settingsUtils'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
interface SettingsSectionsListProps {
|
interface SettingsSectionsListProps {
|
||||||
searchParamTab: SettingsLevel
|
searchParamTab: SettingsLevel
|
||||||
|
@ -11,7 +11,7 @@ import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
|||||||
import usePlatform from '@src/hooks/usePlatform'
|
import usePlatform from '@src/hooks/usePlatform'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { authActor } from '@src/machines/appMachine'
|
import { authActor } from '@src/lib/singletons'
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import type { AxisNames } from '@src/lib/constants'
|
|||||||
import { VIEW_NAMES_SEMANTIC } from '@src/lib/constants'
|
import { VIEW_NAMES_SEMANTIC } from '@src/lib/constants'
|
||||||
import { sceneInfra } from '@src/lib/singletons'
|
import { sceneInfra } from '@src/lib/singletons'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
export function useViewControlMenuItems() {
|
export function useViewControlMenuItems() {
|
||||||
const { state: modelingState, send: modelingSend } = useModelingContext()
|
const { state: modelingState, send: modelingSend } = useModelingContext()
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect } from 'react'
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { useAuthState } from '@src/machines/appMachine'
|
import { useAuthState } from '@src/lib/singletons'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple hook that listens to the auth state of the app and navigates
|
* A simple hook that listens to the auth state of the app and navigates
|
||||||
|
@ -9,7 +9,7 @@ import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from '@src/lib/constants'
|
|||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import type { FileLinkParams } from '@src/lib/links'
|
import type { FileLinkParams } from '@src/lib/links'
|
||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { err, reportRejection } from '@src/lib/trap'
|
import { err, reportRejection } from '@src/lib/trap'
|
||||||
import { getModuleId } from '@src/lib/utils'
|
import { getModuleId } from '@src/lib/utils'
|
||||||
import { engineStreamActor } from '@src/machines/appMachine'
|
import { engineStreamActor } from '@src/lib/singletons'
|
||||||
import { EngineStreamState } from '@src/machines/engineStreamMachine'
|
import { EngineStreamState } from '@src/machines/engineStreamMachine'
|
||||||
import type {
|
import type {
|
||||||
EdgeCutInfo,
|
EdgeCutInfo,
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { useContext } from 'react'
|
|
||||||
|
|
||||||
import { ProjectsMachineContext } from '@src/components/ProjectsContextProvider'
|
|
||||||
|
|
||||||
export const useProjectsContext = () => {
|
|
||||||
return useContext(ProjectsMachineContext)
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { Themes, getSystemTheme } from '@src/lib/theme'
|
import { Themes, getSystemTheme } from '@src/lib/theme'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the current theme based on the theme setting
|
* Resolves the current theme based on the theme setting
|
||||||
|
@ -14,7 +14,6 @@ import { createMachineCommand } from '@src/lib/createMachineCommand'
|
|||||||
import type { authMachine } from '@src/machines/authMachine'
|
import type { authMachine } from '@src/machines/authMachine'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import type { modelingMachine } from '@src/machines/modelingMachine'
|
import type { modelingMachine } from '@src/machines/modelingMachine'
|
||||||
import type { projectsMachine } from '@src/machines/projectsMachine'
|
|
||||||
import type { settingsMachine } from '@src/machines/settingsMachine'
|
import type { settingsMachine } from '@src/machines/settingsMachine'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
@ -22,7 +21,6 @@ export type AllMachines =
|
|||||||
| typeof modelingMachine
|
| typeof modelingMachine
|
||||||
| typeof settingsMachine
|
| typeof settingsMachine
|
||||||
| typeof authMachine
|
| typeof authMachine
|
||||||
| typeof projectsMachine
|
|
||||||
|
|
||||||
interface UseStateMachineCommandsArgs<
|
interface UseStateMachineCommandsArgs<
|
||||||
T extends AllMachines,
|
T extends AllMachines,
|
||||||
|
@ -13,7 +13,7 @@ import { initializeWindowExceptionHandler } from '@src/lib/exceptions'
|
|||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { markOnce } from '@src/lib/performance'
|
import { markOnce } from '@src/lib/performance'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { appActor } from '@src/machines/appMachine'
|
import { appActor } from '@src/lib/singletons'
|
||||||
import reportWebVitals from '@src/reportWebVitals'
|
import reportWebVitals from '@src/reportWebVitals'
|
||||||
|
|
||||||
markOnce('code/willAuth')
|
markOnce('code/willAuth')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { Command } from '@src/lib/commandTypes'
|
import type { Command } from '@src/lib/commandTypes'
|
||||||
import { authActor } from '@src/machines/appMachine'
|
import { authActor } from '@src/lib/singletons'
|
||||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||||
|
|
||||||
export const authCommands: Command[] = [
|
export const authCommands: Command[] = [
|
||||||
|
@ -12,7 +12,7 @@ import type { Command, CommandArgumentOption } from '@src/lib/commandTypes'
|
|||||||
import { engineCommandManager } from '@src/lib/singletons'
|
import { engineCommandManager } from '@src/lib/singletons'
|
||||||
import { err, reportRejection } from '@src/lib/trap'
|
import { err, reportRejection } from '@src/lib/trap'
|
||||||
import { uuidv4 } from '@src/lib/utils'
|
import { uuidv4 } from '@src/lib/utils'
|
||||||
import { getSettings, settingsActor } from '@src/machines/appMachine'
|
import { getSettings, settingsActor } from '@src/lib/singletons'
|
||||||
|
|
||||||
function isWorldCoordinateSystemType(
|
function isWorldCoordinateSystemType(
|
||||||
x: string
|
x: string
|
||||||
|
@ -1,23 +1,15 @@
|
|||||||
import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning'
|
import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning'
|
||||||
import type { StateMachineCommandSetConfig } from '@src/lib/commandTypes'
|
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import type { projectsMachine } from '@src/machines/projectsMachine'
|
import type { Command, CommandArgumentOption } from '@src/lib/commandTypes'
|
||||||
|
|
||||||
|
import {
|
||||||
|
folderSnapshot,
|
||||||
|
defaultProjectFolderNameSnapshot,
|
||||||
|
} from '@src/machines/systemIO/snapshotContext'
|
||||||
|
import { systemIOActor } from '@src/lib/singletons'
|
||||||
|
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||||
|
|
||||||
export type ProjectsCommandSchema = {
|
export type ProjectsCommandSchema = {
|
||||||
'Read projects': Record<string, unknown>
|
|
||||||
'Create project': {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
'Open project': {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
'Delete project': {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
'Rename project': {
|
|
||||||
oldName: string
|
|
||||||
newName: string
|
|
||||||
}
|
|
||||||
'Import file from URL': {
|
'Import file from URL': {
|
||||||
name: string
|
name: string
|
||||||
code?: string
|
code?: string
|
||||||
@ -26,147 +18,243 @@ export type ProjectsCommandSchema = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
export const openProjectCommand: Command = {
|
||||||
typeof projectsMachine,
|
icon: 'arrowRight',
|
||||||
ProjectsCommandSchema
|
name: 'Open project',
|
||||||
> = {
|
displayName: `Open project`,
|
||||||
'Open project': {
|
description: 'Open a project',
|
||||||
icon: 'arrowRight',
|
groupId: 'projects',
|
||||||
description: 'Open a project',
|
needsReview: false,
|
||||||
status: isDesktop() ? 'active' : 'inactive',
|
onSubmit: (record) => {
|
||||||
args: {
|
if (record) {
|
||||||
name: {
|
systemIOActor.send({
|
||||||
inputType: 'options',
|
type: SystemIOMachineEvents.navigateToProject,
|
||||||
required: true,
|
data: { requestedProjectName: record.name },
|
||||||
options: (_, context) =>
|
})
|
||||||
context?.projects.map((p) => ({
|
}
|
||||||
name: p.name,
|
|
||||||
value: p.name,
|
|
||||||
})) || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'Create project': {
|
args: {
|
||||||
icon: 'folderPlus',
|
name: {
|
||||||
description: 'Create a project',
|
required: true,
|
||||||
status: isDesktop() ? 'active' : 'inactive',
|
inputType: 'options',
|
||||||
args: {
|
options: () => {
|
||||||
name: {
|
const folders = folderSnapshot()
|
||||||
inputType: 'string',
|
const options: CommandArgumentOption<string>[] = []
|
||||||
required: true,
|
folders.forEach((folder) => {
|
||||||
defaultValueFromContext: (context) => context.defaultProjectName,
|
options.push({
|
||||||
|
name: folder.name,
|
||||||
|
value: folder.name,
|
||||||
|
isCurrent: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return options
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'Delete project': {
|
|
||||||
icon: 'close',
|
|
||||||
description: 'Delete a project',
|
|
||||||
status: isDesktop() ? 'active' : 'inactive',
|
|
||||||
needsReview: true,
|
|
||||||
reviewMessage: ({ argumentsToSubmit }) =>
|
|
||||||
CommandBarOverwriteWarning({
|
|
||||||
heading: 'Are you sure you want to delete?',
|
|
||||||
message: `This will permanently delete the project "${argumentsToSubmit.name}" and all its contents.`,
|
|
||||||
}),
|
|
||||||
args: {
|
|
||||||
name: {
|
|
||||||
inputType: 'options',
|
|
||||||
required: true,
|
|
||||||
options: (_, context) =>
|
|
||||||
context?.projects.map((p) => ({
|
|
||||||
name: p.name,
|
|
||||||
value: p.name,
|
|
||||||
})) || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'Rename project': {
|
|
||||||
icon: 'folder',
|
|
||||||
description: 'Rename a project',
|
|
||||||
needsReview: true,
|
|
||||||
status: isDesktop() ? 'active' : 'inactive',
|
|
||||||
args: {
|
|
||||||
oldName: {
|
|
||||||
inputType: 'options',
|
|
||||||
required: true,
|
|
||||||
options: (_, context) =>
|
|
||||||
context?.projects.map((p) => ({
|
|
||||||
name: p.name,
|
|
||||||
value: p.name,
|
|
||||||
})) || [],
|
|
||||||
},
|
|
||||||
newName: {
|
|
||||||
inputType: 'string',
|
|
||||||
required: true,
|
|
||||||
defaultValueFromContext: (context) => context.defaultProjectName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'Import file from URL': {
|
|
||||||
icon: 'file',
|
|
||||||
description: 'Create a file',
|
|
||||||
needsReview: true,
|
|
||||||
status: 'active',
|
|
||||||
args: {
|
|
||||||
method: {
|
|
||||||
inputType: 'options',
|
|
||||||
required: true,
|
|
||||||
skip: true,
|
|
||||||
options: isDesktop()
|
|
||||||
? [
|
|
||||||
{ name: 'New project', value: 'newProject' },
|
|
||||||
{ name: 'Existing project', value: 'existingProject' },
|
|
||||||
]
|
|
||||||
: [{ name: 'Overwrite', value: 'existingProject' }],
|
|
||||||
valueSummary(value) {
|
|
||||||
return isDesktop()
|
|
||||||
? value === 'newProject'
|
|
||||||
? 'New project'
|
|
||||||
: 'Existing project'
|
|
||||||
: 'Overwrite'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: We can't get the currently-opened project to auto-populate here because
|
|
||||||
// it's not available on projectMachine, but lower in fileMachine. Unify these.
|
|
||||||
projectName: {
|
|
||||||
inputType: 'options',
|
|
||||||
required: (commandsContext) =>
|
|
||||||
isDesktop() &&
|
|
||||||
commandsContext.argumentsToSubmit.method === 'existingProject',
|
|
||||||
skip: true,
|
|
||||||
options: (_, context) =>
|
|
||||||
context?.projects.map((p) => ({
|
|
||||||
name: p.name,
|
|
||||||
value: p.name,
|
|
||||||
})) || [],
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
inputType: 'string',
|
|
||||||
required: isDesktop(),
|
|
||||||
skip: true,
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
inputType: 'text',
|
|
||||||
required: true,
|
|
||||||
skip: true,
|
|
||||||
valueSummary(value) {
|
|
||||||
const lineCount = value?.trim().split('\n').length
|
|
||||||
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reviewMessage(commandBarContext) {
|
|
||||||
return isDesktop()
|
|
||||||
? `Will add the contents from URL to a new ${
|
|
||||||
commandBarContext.argumentsToSubmit.method === 'newProject'
|
|
||||||
? 'project with file main.kcl'
|
|
||||||
: `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`
|
|
||||||
} named "${
|
|
||||||
commandBarContext.argumentsToSubmit.name
|
|
||||||
}", and set default units to "${
|
|
||||||
commandBarContext.argumentsToSubmit.units
|
|
||||||
}".`
|
|
||||||
: `Will overwrite the contents of the current file with the contents from the URL.`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createProjectCommand: Command = {
|
||||||
|
icon: 'folder',
|
||||||
|
name: 'Create project',
|
||||||
|
displayName: `Create project`,
|
||||||
|
description: 'Create a project',
|
||||||
|
groupId: 'projects',
|
||||||
|
needsReview: false,
|
||||||
|
onSubmit: (record) => {
|
||||||
|
if (record) {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.createProject,
|
||||||
|
data: { requestedProjectName: record.name },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
name: {
|
||||||
|
required: true,
|
||||||
|
inputType: 'string',
|
||||||
|
defaultValue: defaultProjectFolderNameSnapshot,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteProjectCommand: Command = {
|
||||||
|
icon: 'folder',
|
||||||
|
name: 'Delete project',
|
||||||
|
displayName: `Delete project`,
|
||||||
|
description: 'Delete a project',
|
||||||
|
groupId: 'projects',
|
||||||
|
needsReview: true,
|
||||||
|
onSubmit: (record) => {
|
||||||
|
if (record) {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.deleteProject,
|
||||||
|
data: { requestedProjectName: record.name },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reviewMessage: ({ argumentsToSubmit }) =>
|
||||||
|
CommandBarOverwriteWarning({
|
||||||
|
heading: 'Are you sure you want to delete?',
|
||||||
|
message: `This will permanently delete the project "${argumentsToSubmit.name}" and all its contents.`,
|
||||||
|
}),
|
||||||
|
args: {
|
||||||
|
name: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
options: () => {
|
||||||
|
const folders = folderSnapshot()
|
||||||
|
const options: CommandArgumentOption<string>[] = []
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
options.push({
|
||||||
|
name: folder.name,
|
||||||
|
value: folder.name,
|
||||||
|
isCurrent: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renameProjectCommand: Command = {
|
||||||
|
icon: 'folder',
|
||||||
|
name: 'Rename project',
|
||||||
|
displayName: `Rename project`,
|
||||||
|
description: 'Rename a project',
|
||||||
|
groupId: 'projects',
|
||||||
|
needsReview: true,
|
||||||
|
onSubmit: (record) => {
|
||||||
|
if (record) {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.renameProject,
|
||||||
|
data: {
|
||||||
|
requestedProjectName: record.newName,
|
||||||
|
projectName: record.oldName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
oldName: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
options: () => {
|
||||||
|
const folders = folderSnapshot()
|
||||||
|
const options: CommandArgumentOption<string>[] = []
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
options.push({
|
||||||
|
name: folder.name,
|
||||||
|
value: folder.name,
|
||||||
|
isCurrent: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newName: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: true,
|
||||||
|
defaultValue: defaultProjectFolderNameSnapshot,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importFileFromURL: Command = {
|
||||||
|
name: 'Import file from URL',
|
||||||
|
groupId: 'projects',
|
||||||
|
icon: 'file',
|
||||||
|
description: 'Create a file',
|
||||||
|
needsReview: true,
|
||||||
|
onSubmit: (record) => {
|
||||||
|
if (record) {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.importFileFromURL,
|
||||||
|
data: {
|
||||||
|
requestedProjectName: record.projectName,
|
||||||
|
requestedCode: record.code,
|
||||||
|
requestedFileName: record.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
method: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
options: isDesktop()
|
||||||
|
? [
|
||||||
|
{ name: 'New project', value: 'newProject' },
|
||||||
|
{ name: 'Existing project', value: 'existingProject' },
|
||||||
|
]
|
||||||
|
: [{ name: 'Overwrite', value: 'existingProject' }],
|
||||||
|
valueSummary(value) {
|
||||||
|
return isDesktop()
|
||||||
|
? value === 'newProject'
|
||||||
|
? 'New project'
|
||||||
|
: 'Existing project'
|
||||||
|
: 'Overwrite'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO: We can't get the currently-opened project to auto-populate here because
|
||||||
|
// it's not available on projectMachine, but lower in fileMachine. Unify these.
|
||||||
|
projectName: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: (commandsContext) =>
|
||||||
|
isDesktop() &&
|
||||||
|
commandsContext.argumentsToSubmit.method === 'existingProject',
|
||||||
|
skip: true,
|
||||||
|
options: (_, context) => {
|
||||||
|
const folders = folderSnapshot()
|
||||||
|
const options: CommandArgumentOption<string>[] = []
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
options.push({
|
||||||
|
name: folder.name,
|
||||||
|
value: folder.name,
|
||||||
|
isCurrent: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: isDesktop(),
|
||||||
|
skip: true,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
inputType: 'text',
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
valueSummary(value) {
|
||||||
|
const lineCount = value?.trim().split('\n').length
|
||||||
|
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reviewMessage(commandBarContext) {
|
||||||
|
return isDesktop()
|
||||||
|
? `Will add the contents from URL to a new ${
|
||||||
|
commandBarContext.argumentsToSubmit.method === 'newProject'
|
||||||
|
? 'project with file main.kcl'
|
||||||
|
: `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`
|
||||||
|
} named "${
|
||||||
|
commandBarContext.argumentsToSubmit.name
|
||||||
|
}", and set default units to "${
|
||||||
|
commandBarContext.argumentsToSubmit.units
|
||||||
|
}".`
|
||||||
|
: `Will overwrite the contents of the current file with the contents from the URL.`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/** No disk-writing commands are available in the browser */
|
||||||
|
export const projectCommands = isDesktop()
|
||||||
|
? [
|
||||||
|
openProjectCommand,
|
||||||
|
createProjectCommand,
|
||||||
|
deleteProjectCommand,
|
||||||
|
renameProjectCommand,
|
||||||
|
importFileFromURL,
|
||||||
|
]
|
||||||
|
: [importFileFromURL]
|
||||||
|
@ -85,10 +85,23 @@ export async function ensureProjectDirectoryExists(
|
|||||||
return projectDir
|
return projectDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function mkdirOrNOOP(directoryPath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
await window.electron.stat(directoryPath)
|
||||||
|
} catch (e) {
|
||||||
|
if (e === 'ENOENT') {
|
||||||
|
await window.electron.mkdir(directoryPath, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return directoryPath
|
||||||
|
}
|
||||||
|
|
||||||
export async function createNewProjectDirectory(
|
export async function createNewProjectDirectory(
|
||||||
projectName: string,
|
projectName: string,
|
||||||
initialCode?: string,
|
initialCode?: string,
|
||||||
configuration?: DeepPartial<Configuration> | Error
|
configuration?: DeepPartial<Configuration> | Error,
|
||||||
|
initialFileName?: string
|
||||||
): Promise<Project> {
|
): Promise<Project> {
|
||||||
if (!configuration) {
|
if (!configuration) {
|
||||||
configuration = await readAppSettingsFile()
|
configuration = await readAppSettingsFile()
|
||||||
@ -114,7 +127,8 @@ export async function createNewProjectDirectory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectFile = window.electron.path.join(projectDir, PROJECT_ENTRYPOINT)
|
const kclFileName = initialFileName || PROJECT_ENTRYPOINT
|
||||||
|
const projectFile = window.electron.path.join(projectDir, kclFileName)
|
||||||
// When initialCode is present, we're loading existing code. If it's not
|
// When initialCode is present, we're loading existing code. If it's not
|
||||||
// present, we're creating a new project, and we want to incorporate the
|
// present, we're creating a new project, and we want to incorporate the
|
||||||
// user's settings.
|
// user's settings.
|
||||||
|
@ -20,7 +20,7 @@ import type {
|
|||||||
HomeLoaderData,
|
HomeLoaderData,
|
||||||
IndexLoaderData,
|
IndexLoaderData,
|
||||||
} from '@src/lib/types'
|
} from '@src/lib/types'
|
||||||
import { settingsActor } from '@src/machines/appMachine'
|
import { settingsActor } from '@src/lib/singletons'
|
||||||
|
|
||||||
export const telemetryLoader: LoaderFunction = async ({
|
export const telemetryLoader: LoaderFunction = async ({
|
||||||
params,
|
params,
|
||||||
|
@ -43,7 +43,7 @@ import {
|
|||||||
isOverlap,
|
isOverlap,
|
||||||
uuidv4,
|
uuidv4,
|
||||||
} from '@src/lib/utils'
|
} from '@src/lib/utils'
|
||||||
import { engineStreamActor } from '@src/machines/appMachine'
|
import { engineStreamActor } from '@src/lib/singletons'
|
||||||
import type { ModelingMachineEvent } from '@src/machines/modelingMachine'
|
import type { ModelingMachineEvent } from '@src/machines/modelingMachine'
|
||||||
import { showUnsupportedSelectionToast } from '@src/components/ToastUnsupportedSelection'
|
import { showUnsupportedSelectionToast } from '@src/components/ToastUnsupportedSelection'
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ export function readLocalStorageAppSettingsFile():
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readLocalStorageProjectSettingsFile():
|
export function readLocalStorageProjectSettingsFile():
|
||||||
| DeepPartial<ProjectConfiguration>
|
| DeepPartial<ProjectConfiguration>
|
||||||
| Error {
|
| Error {
|
||||||
// TODO: Remove backwards compatibility after a few releases.
|
// TODO: Remove backwards compatibility after a few releases.
|
||||||
@ -456,7 +456,7 @@ export function getSettingInputType(setting: Setting) {
|
|||||||
export const jsAppSettings = async () => {
|
export const jsAppSettings = async () => {
|
||||||
let jsAppSettings = default_app_settings()
|
let jsAppSettings = default_app_settings()
|
||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
const settings = await import('@src/machines/appMachine').then((module) =>
|
const settings = await import('@src/lib/singletons').then((module) =>
|
||||||
module.getSettings()
|
module.getSettings()
|
||||||
)
|
)
|
||||||
if (settings) {
|
if (settings) {
|
||||||
|
@ -9,6 +9,22 @@ import { SceneEntities } from '@src/clientSideScene/sceneEntities'
|
|||||||
import { SceneInfra } from '@src/clientSideScene/sceneInfra'
|
import { SceneInfra } from '@src/clientSideScene/sceneInfra'
|
||||||
import type { BaseUnit } from '@src/lib/settings/settingsTypes'
|
import type { BaseUnit } from '@src/lib/settings/settingsTypes'
|
||||||
|
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { createActor, setup, assign } from 'xstate'
|
||||||
|
|
||||||
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
|
import { createSettings } from '@src/lib/settings/initialSettings'
|
||||||
|
import { authMachine } from '@src/machines/authMachine'
|
||||||
|
import {
|
||||||
|
engineStreamContextCreate,
|
||||||
|
engineStreamMachine,
|
||||||
|
} from '@src/machines/engineStreamMachine'
|
||||||
|
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||||
|
import { settingsMachine } from '@src/machines/settingsMachine'
|
||||||
|
import { systemIOMachineDesktop } from '@src/machines/systemIO/systemIOMachineDesktop'
|
||||||
|
import { systemIOMachineWeb } from '@src/machines/systemIO/systemIOMachineWeb'
|
||||||
|
import type { AppMachineContext } from '@src/lib/types'
|
||||||
|
|
||||||
export const codeManager = new CodeManager()
|
export const codeManager = new CodeManager()
|
||||||
export const engineCommandManager = new EngineCommandManager()
|
export const engineCommandManager = new EngineCommandManager()
|
||||||
export const rustContext = new RustContext(engineCommandManager)
|
export const rustContext = new RustContext(engineCommandManager)
|
||||||
@ -90,3 +106,85 @@ if (typeof window !== 'undefined') {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const { AUTH, SETTINGS, SYSTEM_IO, ENGINE_STREAM } = ACTOR_IDS
|
||||||
|
const appMachineActors = {
|
||||||
|
[AUTH]: authMachine,
|
||||||
|
[SETTINGS]: settingsMachine,
|
||||||
|
[SYSTEM_IO]: isDesktop() ? systemIOMachineDesktop : systemIOMachineWeb,
|
||||||
|
[ENGINE_STREAM]: engineStreamMachine,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const appMachine = setup({
|
||||||
|
types: {} as {
|
||||||
|
context: AppMachineContext
|
||||||
|
},
|
||||||
|
actors: appMachineActors,
|
||||||
|
}).createMachine({
|
||||||
|
id: 'modeling-app',
|
||||||
|
context: {
|
||||||
|
codeManager: codeManager,
|
||||||
|
kclManager: kclManager,
|
||||||
|
engineCommandManager: engineCommandManager,
|
||||||
|
sceneInfra: sceneInfra,
|
||||||
|
sceneEntitiesManager: sceneEntitiesManager,
|
||||||
|
},
|
||||||
|
entry: [
|
||||||
|
assign({
|
||||||
|
// Gotcha, if you use spawn, make sure you remove the ActorRef from context
|
||||||
|
// to prevent memory leaks when the spawned actor is no longer needed
|
||||||
|
authActor: ({ spawn }) => spawn(AUTH, { id: AUTH, systemId: AUTH }),
|
||||||
|
settingsActor: ({ spawn }) =>
|
||||||
|
spawn(SETTINGS, {
|
||||||
|
id: SETTINGS,
|
||||||
|
systemId: SETTINGS,
|
||||||
|
input: createSettings(),
|
||||||
|
}),
|
||||||
|
systemIOActor: ({ spawn }) =>
|
||||||
|
spawn(SYSTEM_IO, { id: SYSTEM_IO, systemId: SYSTEM_IO }),
|
||||||
|
engineStreamActor: ({ spawn }) =>
|
||||||
|
spawn(ENGINE_STREAM, {
|
||||||
|
id: ENGINE_STREAM,
|
||||||
|
systemId: ENGINE_STREAM,
|
||||||
|
input: engineStreamContextCreate(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const appActor = createActor(appMachine, {
|
||||||
|
systemId: 'root',
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GOTCHA: the type coercion of this actor works because it is spawned for
|
||||||
|
* the lifetime of {appActor}, but would not work if it were invoked
|
||||||
|
* or if it were destroyed under any conditions during {appActor}'s life
|
||||||
|
*/
|
||||||
|
export const authActor = appActor.getSnapshot().context.authActor!
|
||||||
|
export const useAuthState = () => useSelector(authActor, (state) => state)
|
||||||
|
export const useToken = () =>
|
||||||
|
useSelector(authActor, (state) => state.context.token)
|
||||||
|
export const useUser = () =>
|
||||||
|
useSelector(authActor, (state) => state.context.user)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GOTCHA: the type coercion of this actor works because it is spawned for
|
||||||
|
* the lifetime of {appActor}, but would not work if it were invoked
|
||||||
|
* or if it were destroyed under any conditions during {appActor}'s life
|
||||||
|
*/
|
||||||
|
export const settingsActor = appActor.getSnapshot().context.settingsActor!
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
export const systemIOActor = appActor.getSnapshot().context.systemIOActor!
|
||||||
|
|
||||||
|
export const engineStreamActor =
|
||||||
|
appActor.getSnapshot().context.engineStreamActor!
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
import type { FileEntry, Project } from '@src/lib/project'
|
import type { FileEntry, Project } from '@src/lib/project'
|
||||||
|
import type CodeManager from '@src/lang/codeManager'
|
||||||
|
import type { EngineCommandManager } from '@src/lang/std/engineConnection'
|
||||||
|
import type { KclManager } from '@src/lang/KclSingleton'
|
||||||
|
import type { SceneInfra } from '@src/clientSideScene/sceneInfra'
|
||||||
|
import type { SceneEntities } from '@src/clientSideScene/sceneEntities'
|
||||||
|
import type { engineStreamMachine } from '@src/machines/engineStreamMachine'
|
||||||
|
import type { authMachine } from '@src/machines/authMachine'
|
||||||
|
import type { settingsMachine } from '@src/machines/settingsMachine'
|
||||||
|
import type { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
||||||
|
import type { ActorRefFrom } from 'xstate'
|
||||||
|
|
||||||
export type IndexLoaderData = {
|
export type IndexLoaderData = {
|
||||||
code: string | null
|
code: string | null
|
||||||
@ -111,3 +121,15 @@ export type AsyncFn<F extends (...args: any[]) => any> = WithReturnType<
|
|||||||
F,
|
F,
|
||||||
Promise<unknown>
|
Promise<unknown>
|
||||||
>
|
>
|
||||||
|
|
||||||
|
export type AppMachineContext = {
|
||||||
|
codeManager: CodeManager
|
||||||
|
kclManager: KclManager
|
||||||
|
engineCommandManager: EngineCommandManager
|
||||||
|
sceneInfra: SceneInfra
|
||||||
|
sceneEntitiesManager: SceneEntities
|
||||||
|
authActor?: ActorRefFrom<typeof authMachine>
|
||||||
|
settingsActor?: ActorRefFrom<typeof settingsMachine>
|
||||||
|
systemIOActor?: ActorRefFrom<typeof systemIOMachine>
|
||||||
|
engineStreamActor?: ActorRefFrom<typeof engineStreamMachine>
|
||||||
|
}
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
import { useSelector } from '@xstate/react'
|
|
||||||
import { createActor, setup, spawnChild } from 'xstate'
|
|
||||||
|
|
||||||
import { createSettings } from '@src/lib/settings/initialSettings'
|
|
||||||
import { authMachine } from '@src/machines/authMachine'
|
|
||||||
import type { EngineStreamActor } from '@src/machines/engineStreamMachine'
|
|
||||||
import {
|
|
||||||
engineStreamContextCreate,
|
|
||||||
engineStreamMachine,
|
|
||||||
} from '@src/machines/engineStreamMachine'
|
|
||||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
|
||||||
import { settingsMachine } from '@src/machines/settingsMachine'
|
|
||||||
|
|
||||||
const { AUTH, SETTINGS, ENGINE_STREAM } = ACTOR_IDS
|
|
||||||
const appMachineActors = {
|
|
||||||
[AUTH]: authMachine,
|
|
||||||
[SETTINGS]: settingsMachine,
|
|
||||||
[ENGINE_STREAM]: engineStreamMachine,
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const appMachine = setup({
|
|
||||||
types: {} as {
|
|
||||||
children: {
|
|
||||||
auth: typeof AUTH
|
|
||||||
settings: typeof SETTINGS
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actors: appMachineActors,
|
|
||||||
}).createMachine({
|
|
||||||
id: 'modeling-app',
|
|
||||||
entry: [
|
|
||||||
spawnChild(AUTH, { id: AUTH, systemId: AUTH }),
|
|
||||||
spawnChild(SETTINGS, {
|
|
||||||
id: SETTINGS,
|
|
||||||
systemId: SETTINGS,
|
|
||||||
input: createSettings(),
|
|
||||||
}),
|
|
||||||
spawnChild(ENGINE_STREAM, {
|
|
||||||
id: ENGINE_STREAM,
|
|
||||||
systemId: ENGINE_STREAM,
|
|
||||||
input: engineStreamContextCreate(),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const appActor = createActor(appMachine)
|
|
||||||
/**
|
|
||||||
* GOTCHA: the type coercion of this actor works because it is spawned for
|
|
||||||
* the lifetime of {appActor}, but would not work if it were invoked
|
|
||||||
* or if it were destroyed under any conditions during {appActor}'s life
|
|
||||||
*/
|
|
||||||
export const authActor = appActor.getSnapshot().children.auth!
|
|
||||||
export const useAuthState = () => useSelector(authActor, (state) => state)
|
|
||||||
export const useToken = () =>
|
|
||||||
useSelector(authActor, (state) => state.context.token)
|
|
||||||
export const useUser = () =>
|
|
||||||
useSelector(authActor, (state) => state.context.user)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GOTCHA: the type coercion of this actor works because it is spawned for
|
|
||||||
* the lifetime of {appActor}, but would not work if it were invoked
|
|
||||||
* or if it were destroyed under any conditions during {appActor}'s life
|
|
||||||
*/
|
|
||||||
export const settingsActor = appActor.getSnapshot().children.settings!
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
export const engineStreamActor = appActor.system.get(
|
|
||||||
ENGINE_STREAM
|
|
||||||
) as EngineStreamActor
|
|
@ -12,6 +12,7 @@ import type {
|
|||||||
KclCommandValue,
|
KclCommandValue,
|
||||||
} from '@src/lib/commandTypes'
|
} from '@src/lib/commandTypes'
|
||||||
import { getCommandArgumentKclValuesOnly } from '@src/lib/commandUtils'
|
import { getCommandArgumentKclValuesOnly } from '@src/lib/commandUtils'
|
||||||
|
import { projectCommands } from '@src/lib/commandBarConfigs/projectsCommandConfig'
|
||||||
|
|
||||||
export type CommandBarContext = {
|
export type CommandBarContext = {
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
@ -661,7 +662,7 @@ function sortCommands(a: Command, b: Command) {
|
|||||||
|
|
||||||
export const commandBarActor = createActor(commandBarMachine, {
|
export const commandBarActor = createActor(commandBarMachine, {
|
||||||
input: {
|
input: {
|
||||||
commands: [...authCommands],
|
commands: [...authCommands, ...projectCommands],
|
||||||
},
|
},
|
||||||
}).start()
|
}).start()
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { engineCommandManager, sceneInfra } from '@src/lib/singletons'
|
|
||||||
import type { MutableRefObject } from 'react'
|
import type { MutableRefObject } from 'react'
|
||||||
import type { ActorRefFrom } from 'xstate'
|
import type { ActorRefFrom } from 'xstate'
|
||||||
import { assign, fromPromise, setup } from 'xstate'
|
import { assign, fromPromise, setup } from 'xstate'
|
||||||
|
import type { AppMachineContext } from '@src/lib/types'
|
||||||
|
|
||||||
export enum EngineStreamState {
|
export enum EngineStreamState {
|
||||||
Off = 'off',
|
Off = 'off',
|
||||||
@ -79,9 +79,13 @@ export const engineStreamMachine = setup({
|
|||||||
actors: {
|
actors: {
|
||||||
[EngineStreamTransition.Play]: fromPromise(
|
[EngineStreamTransition.Play]: fromPromise(
|
||||||
async ({
|
async ({
|
||||||
input: { context, params },
|
input: { context, params, rootContext },
|
||||||
}: {
|
}: {
|
||||||
input: { context: EngineStreamContext; params: { zoomToFit: boolean } }
|
input: {
|
||||||
|
context: EngineStreamContext
|
||||||
|
params: { zoomToFit: boolean }
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
}
|
||||||
}) => {
|
}) => {
|
||||||
const canvas = context.canvasRef.current
|
const canvas = context.canvasRef.current
|
||||||
if (!canvas) return false
|
if (!canvas) return false
|
||||||
@ -98,7 +102,7 @@ export const engineStreamMachine = setup({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await sceneInfra.camControls.restoreRemoteCameraStateAndTriggerSync()
|
await rootContext.sceneInfra.camControls.restoreRemoteCameraStateAndTriggerSync()
|
||||||
|
|
||||||
video.style.display = 'block'
|
video.style.display = 'block'
|
||||||
canvas.style.display = 'none'
|
canvas.style.display = 'none'
|
||||||
@ -108,9 +112,9 @@ export const engineStreamMachine = setup({
|
|||||||
),
|
),
|
||||||
[EngineStreamTransition.Pause]: fromPromise(
|
[EngineStreamTransition.Pause]: fromPromise(
|
||||||
async ({
|
async ({
|
||||||
input: { context },
|
input: { context, rootContext },
|
||||||
}: {
|
}: {
|
||||||
input: { context: EngineStreamContext }
|
input: { context: EngineStreamContext; rootContext: AppMachineContext }
|
||||||
}) => {
|
}) => {
|
||||||
const video = context.videoRef.current
|
const video = context.videoRef.current
|
||||||
if (!video) return
|
if (!video) return
|
||||||
@ -123,7 +127,7 @@ export const engineStreamMachine = setup({
|
|||||||
await holdOntoVideoFrameInCanvas(video, canvas)
|
await holdOntoVideoFrameInCanvas(video, canvas)
|
||||||
video.style.display = 'none'
|
video.style.display = 'none'
|
||||||
|
|
||||||
await sceneInfra.camControls.saveRemoteCameraState()
|
await rootContext.sceneInfra.camControls.saveRemoteCameraState()
|
||||||
|
|
||||||
// Make sure we're on the next frame for no flickering between canvas
|
// Make sure we're on the next frame for no flickering between canvas
|
||||||
// and the video elements.
|
// and the video elements.
|
||||||
@ -138,16 +142,20 @@ export const engineStreamMachine = setup({
|
|||||||
context.mediaStream = null
|
context.mediaStream = null
|
||||||
video.srcObject = null
|
video.srcObject = null
|
||||||
|
|
||||||
engineCommandManager.tearDown({ idleMode: true })
|
rootContext.engineCommandManager.tearDown({ idleMode: true })
|
||||||
})()
|
})()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
[EngineStreamTransition.StartOrReconfigureEngine]: fromPromise(
|
[EngineStreamTransition.StartOrReconfigureEngine]: fromPromise(
|
||||||
async ({
|
async ({
|
||||||
input: { context, event },
|
input: { context, event, rootContext },
|
||||||
}: {
|
}: {
|
||||||
input: { context: EngineStreamContext; event: any }
|
input: {
|
||||||
|
context: EngineStreamContext
|
||||||
|
event: any
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
}
|
||||||
}) => {
|
}) => {
|
||||||
if (!context.authToken) return
|
if (!context.authToken) return
|
||||||
|
|
||||||
@ -172,10 +180,10 @@ export const engineStreamMachine = setup({
|
|||||||
...event.settings,
|
...event.settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
engineCommandManager.settings = settingsNext
|
rootContext.engineCommandManager.settings = settingsNext
|
||||||
|
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
engineCommandManager.start({
|
rootContext.engineCommandManager.start({
|
||||||
setMediaStream: event.onMediaStream,
|
setMediaStream: event.onMediaStream,
|
||||||
setIsStreamReady: (isStreamReady: boolean) => {
|
setIsStreamReady: (isStreamReady: boolean) => {
|
||||||
event.setAppState({ isStreamReady })
|
event.setAppState({ isStreamReady })
|
||||||
@ -225,7 +233,12 @@ export const engineStreamMachine = setup({
|
|||||||
reenter: true,
|
reenter: true,
|
||||||
invoke: {
|
invoke: {
|
||||||
src: EngineStreamTransition.StartOrReconfigureEngine,
|
src: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
input: (args) => args,
|
input: (args) => ({
|
||||||
|
context: args.context,
|
||||||
|
rootContext: args.self.system.get('root').getSnapshot().context,
|
||||||
|
params: { zoomToFit: args.context.zoomToFit },
|
||||||
|
event: args.event,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
// Transition requested by engineConnection
|
// Transition requested by engineConnection
|
||||||
@ -246,6 +259,7 @@ export const engineStreamMachine = setup({
|
|||||||
src: EngineStreamTransition.Play,
|
src: EngineStreamTransition.Play,
|
||||||
input: (args) => ({
|
input: (args) => ({
|
||||||
context: args.context,
|
context: args.context,
|
||||||
|
rootContext: args.self.system.get('root').getSnapshot().context,
|
||||||
params: { zoomToFit: args.context.zoomToFit },
|
params: { zoomToFit: args.context.zoomToFit },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -261,7 +275,11 @@ export const engineStreamMachine = setup({
|
|||||||
[EngineStreamState.Reconfiguring]: {
|
[EngineStreamState.Reconfiguring]: {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: EngineStreamTransition.StartOrReconfigureEngine,
|
src: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
input: (args) => args,
|
input: (args) => ({
|
||||||
|
context: args.context,
|
||||||
|
rootContext: args.self.system.get('root').getSnapshot().context,
|
||||||
|
event: args.event,
|
||||||
|
}),
|
||||||
onDone: {
|
onDone: {
|
||||||
target: EngineStreamState.Playing,
|
target: EngineStreamState.Playing,
|
||||||
},
|
},
|
||||||
@ -270,7 +288,10 @@ export const engineStreamMachine = setup({
|
|||||||
[EngineStreamState.Paused]: {
|
[EngineStreamState.Paused]: {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: EngineStreamTransition.Pause,
|
src: EngineStreamTransition.Pause,
|
||||||
input: (args) => args,
|
input: (args) => ({
|
||||||
|
context: args.context,
|
||||||
|
rootContext: args.self.system.get('root').getSnapshot().context,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||||
@ -282,7 +303,11 @@ export const engineStreamMachine = setup({
|
|||||||
reenter: true,
|
reenter: true,
|
||||||
invoke: {
|
invoke: {
|
||||||
src: EngineStreamTransition.StartOrReconfigureEngine,
|
src: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
input: (args) => args,
|
input: (args) => ({
|
||||||
|
context: args.context,
|
||||||
|
rootContext: args.self.system.get('root').getSnapshot().context,
|
||||||
|
event: args.event,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
// The stream can be paused as it's resuming.
|
// The stream can be paused as it's resuming.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const ACTOR_IDS = {
|
export const ACTOR_IDS = {
|
||||||
AUTH: 'auth',
|
AUTH: 'auth',
|
||||||
SETTINGS: 'settings',
|
SETTINGS: 'settings',
|
||||||
|
SYSTEM_IO: 'systemIO',
|
||||||
ENGINE_STREAM: 'engine_stream',
|
ENGINE_STREAM: 'engine_stream',
|
||||||
} as const
|
} as const
|
||||||
|
@ -1,337 +0,0 @@
|
|||||||
import { assign, fromPromise, setup } from 'xstate'
|
|
||||||
|
|
||||||
import type { ProjectsCommandSchema } from '@src/lib/commandBarConfigs/projectsCommandConfig'
|
|
||||||
import type { Project } from '@src/lib/project'
|
|
||||||
import { isArray } from '@src/lib/utils'
|
|
||||||
|
|
||||||
export const projectsMachine = setup({
|
|
||||||
types: {
|
|
||||||
context: {} as {
|
|
||||||
projects: Project[]
|
|
||||||
defaultProjectName: string
|
|
||||||
defaultDirectory: string
|
|
||||||
hasListedProjects: boolean
|
|
||||||
},
|
|
||||||
events: {} as
|
|
||||||
| { type: 'Read projects'; data: ProjectsCommandSchema['Read projects'] }
|
|
||||||
| { type: 'Open project'; data: ProjectsCommandSchema['Open project'] }
|
|
||||||
| {
|
|
||||||
type: 'Rename project'
|
|
||||||
data: ProjectsCommandSchema['Rename project']
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'Create project'
|
|
||||||
data: ProjectsCommandSchema['Create project']
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'Delete project'
|
|
||||||
data: ProjectsCommandSchema['Delete project']
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'Import file from URL'
|
|
||||||
data: ProjectsCommandSchema['Import file from URL']
|
|
||||||
}
|
|
||||||
| { type: 'navigate'; data: { name: string } }
|
|
||||||
| {
|
|
||||||
type: 'xstate.done.actor.read-projects'
|
|
||||||
output: Project[]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'xstate.done.actor.delete-project'
|
|
||||||
output: { message: string; name: string }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'xstate.done.actor.create-project'
|
|
||||||
output: { message: string; name: string }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'xstate.done.actor.rename-project'
|
|
||||||
output: { message: string; oldName: string; newName: string }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'xstate.done.actor.create-file'
|
|
||||||
output: { message: string; projectName: string; fileName: string }
|
|
||||||
}
|
|
||||||
| { type: 'assign'; data: { [key: string]: any } },
|
|
||||||
input: {} as {
|
|
||||||
projects: Project[]
|
|
||||||
defaultProjectName: string
|
|
||||||
defaultDirectory: string
|
|
||||||
hasListedProjects: boolean
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
setProjects: assign({
|
|
||||||
projects: ({ context, event }) =>
|
|
||||||
'output' in event && isArray(event.output)
|
|
||||||
? event.output
|
|
||||||
: context.projects,
|
|
||||||
}),
|
|
||||||
setHasListedProjects: assign({
|
|
||||||
hasListedProjects: () => true,
|
|
||||||
}),
|
|
||||||
toastSuccess: () => {},
|
|
||||||
toastError: () => {},
|
|
||||||
navigateToProject: () => {},
|
|
||||||
navigateToProjectIfNeeded: () => {},
|
|
||||||
navigateToFile: () => {},
|
|
||||||
},
|
|
||||||
actors: {
|
|
||||||
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
|
||||||
createProject: fromPromise(
|
|
||||||
(_: { input: { name: string; projects: Project[] } }) =>
|
|
||||||
Promise.resolve({ message: '' })
|
|
||||||
),
|
|
||||||
renameProject: fromPromise(
|
|
||||||
(_: {
|
|
||||||
input: {
|
|
||||||
oldName: string
|
|
||||||
newName: string
|
|
||||||
defaultProjectName: string
|
|
||||||
defaultDirectory: string
|
|
||||||
projects: Project[]
|
|
||||||
}
|
|
||||||
}) =>
|
|
||||||
Promise.resolve({
|
|
||||||
message: '',
|
|
||||||
oldName: '',
|
|
||||||
newName: '',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
deleteProject: fromPromise(
|
|
||||||
(_: { input: { defaultDirectory: string; name: string } }) =>
|
|
||||||
Promise.resolve({
|
|
||||||
message: '',
|
|
||||||
name: '',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
createFile: fromPromise(
|
|
||||||
(_: {
|
|
||||||
input: ProjectsCommandSchema['Import file from URL'] & {
|
|
||||||
projects: Project[]
|
|
||||||
}
|
|
||||||
}) => Promise.resolve({ message: '', projectName: '', fileName: '' })
|
|
||||||
),
|
|
||||||
},
|
|
||||||
guards: {
|
|
||||||
'Has at least 1 project': ({ event }) => {
|
|
||||||
if (event.type !== 'xstate.done.actor.read-projects') return false
|
|
||||||
return event.output.length ? event.output.length >= 1 : false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).createMachine({
|
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjAHYAbADoArBJVMFTCQA4mTAMwmxAXwsy0mHARLkKASXRcATjywAzYgBtsb3cMLABVACUAGWY2JBAuXn5BONEESRl5BAUFFQM1HQUxJSYATmyFAyUTKxsMbDwiMjA1ZGosUlQsDmCAKzB8HlgKcLBcCC7e-sGYoQTiPgEhVIUy9SKSiQ0NEwkclRyMxGKJNRKlMRVDU23zpRqQW3qHJpa2jonUPoGhgGF3UZ42G6nymMzicwWyVAyzKJzECg0OiYGjERhK+kOWUMSlOGiUlTEJlMxRKBnuj3sjXIr1gHy+g2Go3GwPpsDBnG48ySS0QGj0+Q0JTOGkqBg2JhKmNRJy2OyUEn0Kml5LqlMczVatJZUyGI1IuDs2oG7PinMhPIQfKYAqFShF+PFkrkRwkYjU8OUShKKhMKg0pUs1geqoa6ppdJ1FD+AKBk2NrFmZu5KV5-L9tvtYokEsxelRagUCoMiMumyUdpVdlDL01Ee+FAAImAAoC6zwTRDk9DU9b08LRY7MWd1AZcoS-aoLiYFJWnlSNW0jQyAPIcMCkNsdpOLFOWtOC-sO7NOzLbGWFbY6acmFESWdql7R3B8UhQNsUCACZpkABuqAA1s0+D-M+YAALRLluiQ7t2WRMGIbryjeBgGBIEglNsCi5sY6hMOchQqEoCLFCh97VtST4vm+S4UGA7jBO4agcH4z7eKg7joGowExhBcbtgm4LblCIiKPBiHZiKqHoZhuaFhoBbGMYBhiPBCgStUQYUuRzR6gaZDUXxH5fmov4Ac0-z6pgvEgvGsQctBwnLGJahIZJaEYdOmKEfJuQSoRRKSRcZHPNSunoPp750QxTEsTwbEcWoFkGuBkECfZXIwSJcEIS5Ekoe5MnOggXqIaUqFiiUhIInemkhiFzRNi2EU0Z+1KmYBagQM2YCAtZ9JQRljmiTlrn5dJnlFShOLwcpZjKBs+KBrUVb1WojU9c1hlRexMWsexnFdS2KV8QN5q7laNqHlmOaTcpmjoWc8L6HoYrBfOagjGMm02QyrXfqQf4dSBEB9Tqp1dllebichUkeVhRXTiU+QecUKhCps4pvWGn0QN9rJGW1ANmYlTKg98DAKHZpoORanoyqh8oImU3pKJifJ5Ohdr7CYqjIvKWMvDjeORttjHMXtCXA2T0xpdTg20+W9MSIzgorIRXlpr6ORbPs+jwQLFEgVRPj+JQf0mUTHXcaBYG+AE4OZcsxbuts5hbCK+zFmImJgSc5zPV6BQVD6RQG80lERXblCi7tcX7VxRvgVHDtDQgaE4vK2gXKiTA6ItubmJo8IoqOylERUVhBh0XXwHEWn1YmNO7mBKg+2KpzK2IpIBUwBhqUtwYre9tbvEutfpWdsFFnkPpVJnQqz15Ozur36FoypREbGH4Zj438u7tO1qIu5qK5PBGyYjebpEkS44e9oVTbxHr5tnvk9ZeXajTuW+zaHNuzYRUmoeCEp-RKlUHJbeYVhYDDfhDVIxR5IGGnISQk6E+6EkxMUBQX9c66EMMg3Q5Zt7rWNkuOBjsjilDUAqQsZQzzgOkJNUk7o7SmF-tiXOUCmQwMGBQ1OF4TgVGzFUG8yIxQGDZiYPI4oqh+mPsrDSy05xhmfm+KO-CLT+iRucEUuwFQqWzMWXM2YTAuTtL6ZBylkRcMrkAA */
|
|
||||||
id: 'Home machine',
|
|
||||||
|
|
||||||
initial: 'Reading projects',
|
|
||||||
|
|
||||||
context: ({ input }) => ({
|
|
||||||
...input,
|
|
||||||
}),
|
|
||||||
|
|
||||||
on: {
|
|
||||||
assign: {
|
|
||||||
actions: assign(({ event }) => ({
|
|
||||||
...event.data,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
|
|
||||||
'Import file from URL': '.Creating file',
|
|
||||||
},
|
|
||||||
states: {
|
|
||||||
'Has no projects': {
|
|
||||||
on: {
|
|
||||||
'Read projects': {
|
|
||||||
target: 'Reading projects',
|
|
||||||
},
|
|
||||||
'Create project': {
|
|
||||||
target: 'Creating project',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Has projects': {
|
|
||||||
on: {
|
|
||||||
'Read projects': {
|
|
||||||
target: 'Reading projects',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Rename project': {
|
|
||||||
target: 'Renaming project',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Create project': {
|
|
||||||
target: 'Creating project',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Delete project': {
|
|
||||||
target: 'Deleting project',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Open project': {
|
|
||||||
target: 'Reading projects',
|
|
||||||
actions: 'navigateToProject',
|
|
||||||
reenter: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Creating project': {
|
|
||||||
invoke: {
|
|
||||||
id: 'create-project',
|
|
||||||
src: 'createProject',
|
|
||||||
input: ({ event, context }) => {
|
|
||||||
if (
|
|
||||||
event.type !== 'Create project' &&
|
|
||||||
event.type !== 'Import file from URL'
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
name: '',
|
|
||||||
projects: context.projects,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: event.data.name,
|
|
||||||
projects: context.projects,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
target: 'Reading projects',
|
|
||||||
actions: ['toastSuccess', 'navigateToProject'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: 'Reading projects',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Renaming project': {
|
|
||||||
invoke: {
|
|
||||||
id: 'rename-project',
|
|
||||||
src: 'renameProject',
|
|
||||||
input: ({ event, context }) => {
|
|
||||||
if (event.type !== 'Rename project') {
|
|
||||||
// This is to make TS happy
|
|
||||||
return {
|
|
||||||
defaultProjectName: context.defaultProjectName,
|
|
||||||
defaultDirectory: context.defaultDirectory,
|
|
||||||
oldName: '',
|
|
||||||
newName: '',
|
|
||||||
projects: context.projects,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
defaultProjectName: context.defaultProjectName,
|
|
||||||
defaultDirectory: context.defaultDirectory,
|
|
||||||
oldName: event.data.oldName,
|
|
||||||
newName: event.data.newName,
|
|
||||||
projects: context.projects,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
target: '#Home machine.Reading projects',
|
|
||||||
actions: ['toastSuccess', 'navigateToProjectIfNeeded'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: '#Home machine.Reading projects',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Deleting project': {
|
|
||||||
invoke: {
|
|
||||||
id: 'delete-project',
|
|
||||||
src: 'deleteProject',
|
|
||||||
input: ({ event, context }) => {
|
|
||||||
if (event.type !== 'Delete project') {
|
|
||||||
// This is to make TS happy
|
|
||||||
return {
|
|
||||||
defaultDirectory: context.defaultDirectory,
|
|
||||||
name: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
defaultDirectory: context.defaultDirectory,
|
|
||||||
name: event.data.name,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
actions: ['toastSuccess', 'navigateToProjectIfNeeded'],
|
|
||||||
target: '#Home machine.Reading projects',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: {
|
|
||||||
actions: ['toastError'],
|
|
||||||
target: '#Home machine.Has projects',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Reading projects': {
|
|
||||||
invoke: {
|
|
||||||
id: 'read-projects',
|
|
||||||
src: 'readProjects',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
guard: 'Has at least 1 project',
|
|
||||||
target: 'Has projects',
|
|
||||||
actions: ['setProjects', 'setHasListedProjects'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: 'Has no projects',
|
|
||||||
actions: ['setProjects', 'setHasListedProjects'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: 'Has no projects',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Creating file': {
|
|
||||||
invoke: {
|
|
||||||
id: 'create-file',
|
|
||||||
src: 'createFile',
|
|
||||||
input: ({ event, context }) => {
|
|
||||||
if (event.type !== 'Import file from URL') {
|
|
||||||
return {
|
|
||||||
code: '',
|
|
||||||
name: '',
|
|
||||||
method: 'existingProject',
|
|
||||||
projects: context.projects,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
code: event.data.code || '',
|
|
||||||
name: event.data.name,
|
|
||||||
method: event.data.method,
|
|
||||||
projectName: event.data.projectName,
|
|
||||||
projects: context.projects,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: {
|
|
||||||
target: 'Reading projects',
|
|
||||||
actions: ['navigateToFile', 'toastSuccess'],
|
|
||||||
},
|
|
||||||
onError: {
|
|
||||||
target: 'Reading projects',
|
|
||||||
actions: 'toastError',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
@ -35,13 +35,6 @@ import {
|
|||||||
saveSettings,
|
saveSettings,
|
||||||
setSettingsAtLevel,
|
setSettingsAtLevel,
|
||||||
} from '@src/lib/settings/settingsUtils'
|
} from '@src/lib/settings/settingsUtils'
|
||||||
import {
|
|
||||||
codeManager,
|
|
||||||
engineCommandManager,
|
|
||||||
kclManager,
|
|
||||||
sceneEntitiesManager,
|
|
||||||
sceneInfra,
|
|
||||||
} from '@src/lib/singletons'
|
|
||||||
import {
|
import {
|
||||||
Themes,
|
Themes,
|
||||||
darkModeMatcher,
|
darkModeMatcher,
|
||||||
@ -95,13 +88,14 @@ export const settingsMachine = setup({
|
|||||||
doNotPersist: boolean
|
doNotPersist: boolean
|
||||||
context: SettingsMachineContext
|
context: SettingsMachineContext
|
||||||
toastCallback?: () => void
|
toastCallback?: () => void
|
||||||
|
rootContext: any
|
||||||
}
|
}
|
||||||
>(async ({ input }) => {
|
>(async ({ input }) => {
|
||||||
// Without this, when a user changes the file, it'd
|
// Without this, when a user changes the file, it'd
|
||||||
// create a detection loop with the file-system watcher.
|
// create a detection loop with the file-system watcher.
|
||||||
if (input.doNotPersist) return
|
if (input.doNotPersist) return
|
||||||
|
|
||||||
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
|
input.rootContext.codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
|
||||||
const { currentProject, ...settings } = input.context
|
const { currentProject, ...settings } = input.context
|
||||||
|
|
||||||
const val = await saveSettings(settings, currentProject?.path)
|
const val = await saveSettings(settings, currentProject?.path)
|
||||||
@ -190,20 +184,28 @@ export const settingsMachine = setup({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setEngineTheme: ({ context }) => {
|
setEngineTheme: ({ context, self }) => {
|
||||||
|
const rootContext = self.system.get('root').getSnapshot().context
|
||||||
|
const engineCommandManager = rootContext.engineCommandManager
|
||||||
if (engineCommandManager && context.app.theme.current) {
|
if (engineCommandManager && context.app.theme.current) {
|
||||||
engineCommandManager
|
engineCommandManager
|
||||||
.setTheme(context.app.theme.current)
|
.setTheme(context.app.theme.current)
|
||||||
.catch(reportRejection)
|
.catch(reportRejection)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setClientTheme: ({ context }) => {
|
setClientTheme: ({ context, self }) => {
|
||||||
|
const rootContext = self.system.get('root').getSnapshot().context
|
||||||
|
const sceneInfra = rootContext.sceneInfra
|
||||||
|
const sceneEntitiesManager = rootContext.sceneEntitiesManager
|
||||||
|
|
||||||
if (!sceneInfra || !sceneEntitiesManager) return
|
if (!sceneInfra || !sceneEntitiesManager) return
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
sceneInfra.theme = opposingTheme
|
sceneInfra.theme = opposingTheme
|
||||||
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
||||||
},
|
},
|
||||||
setAllowOrbitInSketchMode: ({ context }) => {
|
setAllowOrbitInSketchMode: ({ context, self }) => {
|
||||||
|
const rootContext = self.system.get('root').getSnapshot().context
|
||||||
|
const sceneInfra = rootContext.sceneInfra
|
||||||
if (!sceneInfra.camControls) return
|
if (!sceneInfra.camControls) return
|
||||||
sceneInfra.camControls._setting_allowOrbitInSketchMode =
|
sceneInfra.camControls._setting_allowOrbitInSketchMode =
|
||||||
context.app.allowOrbitInSketchMode.current
|
context.app.allowOrbitInSketchMode.current
|
||||||
@ -232,7 +234,9 @@ export const settingsMachine = setup({
|
|||||||
id: `${event.type}.success`,
|
id: `${event.type}.success`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
'Execute AST': ({ context, event }) => {
|
'Execute AST': ({ context, event, self }) => {
|
||||||
|
const rootContext = self.system.get('root').getSnapshot().context
|
||||||
|
const kclManager = rootContext.kclManager
|
||||||
try {
|
try {
|
||||||
const relevantSetting = (s: typeof settings) => {
|
const relevantSetting = (s: typeof settings) => {
|
||||||
return (
|
return (
|
||||||
@ -345,8 +349,10 @@ export const settingsMachine = setup({
|
|||||||
currentTheme === Themes.System ? getSystemTheme() : currentTheme
|
currentTheme === Themes.System ? getSystemTheme() : currentTheme
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
setEngineCameraProjection: ({ context }) => {
|
setEngineCameraProjection: ({ context, self }) => {
|
||||||
const newCurrentProjection = context.modeling.cameraProjection.current
|
const newCurrentProjection = context.modeling.cameraProjection.current
|
||||||
|
const rootContext = self.system.get('root').getSnapshot().context
|
||||||
|
const sceneInfra = rootContext.sceneInfra
|
||||||
sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection)
|
sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection)
|
||||||
},
|
},
|
||||||
sendThemeToWatcher: sendTo('watchSystemTheme', ({ context }) => ({
|
sendThemeToWatcher: sendTo('watchSystemTheme', ({ context }) => ({
|
||||||
@ -532,7 +538,7 @@ export const settingsMachine = setup({
|
|||||||
console.error('Error persisting settings')
|
console.error('Error persisting settings')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
input: ({ context, event }) => {
|
input: ({ context, event, self }) => {
|
||||||
if (
|
if (
|
||||||
event.type === 'set.app.namedViews' &&
|
event.type === 'set.app.namedViews' &&
|
||||||
'toastCallback' in event.data
|
'toastCallback' in event.data
|
||||||
@ -541,12 +547,14 @@ export const settingsMachine = setup({
|
|||||||
doNotPersist: event.doNotPersist ?? false,
|
doNotPersist: event.doNotPersist ?? false,
|
||||||
context,
|
context,
|
||||||
toastCallback: event.data.toastCallback,
|
toastCallback: event.data.toastCallback,
|
||||||
|
rootContext: self.system.get('root').getSnapshot().context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
doNotPersist: event.doNotPersist ?? false,
|
doNotPersist: event.doNotPersist ?? false,
|
||||||
context,
|
context,
|
||||||
|
rootContext: self.system.get('root').getSnapshot().context,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
21
src/machines/systemIO/hooks.ts
Normal file
21
src/machines/systemIO/hooks.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { systemIOActor } from '@src/lib/singletons'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
export const useRequestedProjectName = () =>
|
||||||
|
useSelector(systemIOActor, (state) => state.context.requestedProjectName)
|
||||||
|
export const useRequestedFileName = () =>
|
||||||
|
useSelector(systemIOActor, (state) => state.context.requestedFileName)
|
||||||
|
export const useProjectDirectoryPath = () =>
|
||||||
|
useSelector(systemIOActor, (state) => state.context.projectDirectoryPath)
|
||||||
|
export const useFolders = () =>
|
||||||
|
useSelector(systemIOActor, (state) => state.context.folders)
|
||||||
|
export const useState = () => useSelector(systemIOActor, (state) => state)
|
||||||
|
export const useCanReadWriteProjectDirectory = () =>
|
||||||
|
useSelector(
|
||||||
|
systemIOActor,
|
||||||
|
(state) => state.context.canReadWriteProjectDirectory
|
||||||
|
)
|
||||||
|
export const useHasListedProjects = () =>
|
||||||
|
useSelector(systemIOActor, (state) => state.context.hasListedProjects)
|
||||||
|
|
||||||
|
export const useClearURLParams = () =>
|
||||||
|
useSelector(systemIOActor, (state) => state.context.clearURLParams)
|
11
src/machines/systemIO/snapshotContext.ts
Normal file
11
src/machines/systemIO/snapshotContext.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { systemIOActor } from '@src/lib/singletons'
|
||||||
|
|
||||||
|
export const folderSnapshot = () => {
|
||||||
|
const { folders } = systemIOActor.getSnapshot().context
|
||||||
|
return folders
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultProjectFolderNameSnapshot = () => {
|
||||||
|
const { defaultProjectFolderName } = systemIOActor.getSnapshot().context
|
||||||
|
return defaultProjectFolderName
|
||||||
|
}
|
78
src/machines/systemIO/systemIOMachine.test.ts
Normal file
78
src/machines/systemIO/systemIOMachine.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { DEFAULT_PROJECT_NAME } from '@src/lib/constants'
|
||||||
|
import { systemIOMachineDesktop } from '@src/machines/systemIO/systemIOMachineDesktop'
|
||||||
|
import {
|
||||||
|
NO_PROJECT_DIRECTORY,
|
||||||
|
SystemIOMachineEvents,
|
||||||
|
SystemIOMachineStates,
|
||||||
|
} from '@src/machines/systemIO/utils'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { createActor, waitFor } from 'xstate'
|
||||||
|
|
||||||
|
describe('systemIOMachine - XState', () => {
|
||||||
|
describe('desktop', () => {
|
||||||
|
describe('when initializied', () => {
|
||||||
|
it('should contain the default context values', () => {
|
||||||
|
const actor = createActor(systemIOMachineDesktop).start()
|
||||||
|
const context = actor.getSnapshot().context
|
||||||
|
expect(context.folders).toStrictEqual([])
|
||||||
|
expect(context.defaultProjectFolderName).toStrictEqual(
|
||||||
|
DEFAULT_PROJECT_NAME
|
||||||
|
)
|
||||||
|
expect(context.projectDirectoryPath).toBe(NO_PROJECT_DIRECTORY)
|
||||||
|
expect(context.hasListedProjects).toBe(false)
|
||||||
|
expect(context.requestedProjectName).toStrictEqual({
|
||||||
|
name: NO_PROJECT_DIRECTORY,
|
||||||
|
})
|
||||||
|
expect(context.requestedFileName).toStrictEqual({
|
||||||
|
project: NO_PROJECT_DIRECTORY,
|
||||||
|
file: NO_PROJECT_DIRECTORY,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('should be in idle state', () => {
|
||||||
|
const actor = createActor(systemIOMachineDesktop).start()
|
||||||
|
const state = actor.getSnapshot().value
|
||||||
|
expect(state).toBe(SystemIOMachineStates.idle)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('when reading projects', () => {
|
||||||
|
it('should exit early when project directory is empty string', async () => {
|
||||||
|
const actor = createActor(systemIOMachineDesktop).start()
|
||||||
|
actor.send({
|
||||||
|
type: SystemIOMachineEvents.readFoldersFromProjectDirectory,
|
||||||
|
})
|
||||||
|
await waitFor(actor, (state) =>
|
||||||
|
state.matches(SystemIOMachineStates.readingFolders)
|
||||||
|
)
|
||||||
|
await waitFor(actor, (state) =>
|
||||||
|
state.matches(SystemIOMachineStates.idle)
|
||||||
|
)
|
||||||
|
const context = actor.getSnapshot().context
|
||||||
|
expect(context.folders).toStrictEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('when setting project directory path', () => {
|
||||||
|
it('should set new project directory path', async () => {
|
||||||
|
const kclSamplesPath = path.join('public', 'kcl-samples')
|
||||||
|
const actor = createActor(systemIOMachineDesktop).start()
|
||||||
|
actor.send({
|
||||||
|
type: SystemIOMachineEvents.setProjectDirectoryPath,
|
||||||
|
data: { requestedProjectDirectoryPath: kclSamplesPath },
|
||||||
|
})
|
||||||
|
let context = actor.getSnapshot().context
|
||||||
|
expect(context.projectDirectoryPath).toBe(kclSamplesPath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('when setting default project folder name', () => {
|
||||||
|
it('should set a new default project folder name', async () => {
|
||||||
|
const expected = 'coolcoolcoolProjectName'
|
||||||
|
const actor = createActor(systemIOMachineDesktop).start()
|
||||||
|
actor.send({
|
||||||
|
type: SystemIOMachineEvents.setDefaultProjectFolderName,
|
||||||
|
data: { requestedDefaultProjectFolderName: expected },
|
||||||
|
})
|
||||||
|
let context = actor.getSnapshot().context
|
||||||
|
expect(context.defaultProjectFolderName).toBe(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
448
src/machines/systemIO/systemIOMachine.ts
Normal file
448
src/machines/systemIO/systemIOMachine.ts
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
import { DEFAULT_PROJECT_NAME } from '@src/lib/constants'
|
||||||
|
import type { Project } from '@src/lib/project'
|
||||||
|
import type { SystemIOContext } from '@src/machines/systemIO/utils'
|
||||||
|
import {
|
||||||
|
NO_PROJECT_DIRECTORY,
|
||||||
|
SystemIOMachineActions,
|
||||||
|
SystemIOMachineActors,
|
||||||
|
SystemIOMachineEvents,
|
||||||
|
SystemIOMachineStates,
|
||||||
|
} from '@src/machines/systemIO/utils'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { assertEvent, assign, fromPromise, setup } from 'xstate'
|
||||||
|
import type { AppMachineContext } from '@src/lib/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles any system level I/O for folders and files
|
||||||
|
* This machine will be initializes once within the applications runtime
|
||||||
|
* and exist for the entire life cycle of the application and able to be access
|
||||||
|
* at a global level.
|
||||||
|
*/
|
||||||
|
export const systemIOMachine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as SystemIOContext,
|
||||||
|
events: {} as
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.readFoldersFromProjectDirectory
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.done_readFoldersFromProjectDirectory
|
||||||
|
output: Project[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.done_checkReadWrite
|
||||||
|
output: { value: boolean; error: unknown }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.setProjectDirectoryPath
|
||||||
|
data: { requestedProjectDirectoryPath: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.navigateToProject
|
||||||
|
data: { requestedProjectName: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.navigateToFile
|
||||||
|
data: { requestedProjectName: string; requestedFileName: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.createProject
|
||||||
|
data: { requestedProjectName: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.renameProject
|
||||||
|
data: { requestedProjectName: string; projectName: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.deleteProject
|
||||||
|
data: { requestedProjectName: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.createKCLFile
|
||||||
|
data: {
|
||||||
|
requestedProjectName: string
|
||||||
|
requestedFileName: string
|
||||||
|
requestedCode: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.importFileFromURL
|
||||||
|
data: {
|
||||||
|
requestedProjectName: string
|
||||||
|
requestedFileName: string
|
||||||
|
requestedCode: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.setDefaultProjectFolderName
|
||||||
|
data: { requestedDefaultProjectFolderName: string }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
[SystemIOMachineActions.setFolders]: assign({
|
||||||
|
folders: ({ event }) => {
|
||||||
|
assertEvent(
|
||||||
|
event,
|
||||||
|
SystemIOMachineEvents.done_readFoldersFromProjectDirectory
|
||||||
|
)
|
||||||
|
return event.output
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[SystemIOMachineActions.setProjectDirectoryPath]: assign({
|
||||||
|
projectDirectoryPath: ({ event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.setProjectDirectoryPath)
|
||||||
|
return event.data.requestedProjectDirectoryPath
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[SystemIOMachineActions.setRequestedProjectName]: assign({
|
||||||
|
requestedProjectName: ({ event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.navigateToProject)
|
||||||
|
return { name: event.data.requestedProjectName }
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[SystemIOMachineActions.setRequestedFileName]: assign({
|
||||||
|
requestedFileName: ({ event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.navigateToFile)
|
||||||
|
return {
|
||||||
|
project: event.data.requestedProjectName,
|
||||||
|
file: event.data.requestedFileName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[SystemIOMachineActions.setDefaultProjectFolderName]: assign({
|
||||||
|
defaultProjectFolderName: ({ event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.setDefaultProjectFolderName)
|
||||||
|
return event.data.requestedDefaultProjectFolderName
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[SystemIOMachineActions.toastSuccess]: ({ event }) => {
|
||||||
|
toast.success(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
'message' in event.output &&
|
||||||
|
typeof event.output.message === 'string' &&
|
||||||
|
event.output.message) ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[SystemIOMachineActions.toastError]: ({ event }) => {
|
||||||
|
toast.error(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
typeof event.output === 'string' &&
|
||||||
|
event.output) ||
|
||||||
|
('error' in event &&
|
||||||
|
event.error instanceof Error &&
|
||||||
|
event.error.message) ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[SystemIOMachineActions.setReadWriteProjectDirectory]: assign({
|
||||||
|
canReadWriteProjectDirectory: ({ event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.done_checkReadWrite)
|
||||||
|
return event.output
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
[SystemIOMachineActors.readFoldersFromProjectDirectory]: fromPromise(
|
||||||
|
async ({ input: context }: { input: SystemIOContext }) => {
|
||||||
|
const folders: Project[] = []
|
||||||
|
return folders
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.createProject]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input: { context, requestedProjectName },
|
||||||
|
}: {
|
||||||
|
input: { context: SystemIOContext; requestedProjectName: string }
|
||||||
|
}) => {
|
||||||
|
return { message: '', name: '' }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.deleteProject]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input: { context, requestedProjectName },
|
||||||
|
}: {
|
||||||
|
input: { context: SystemIOContext; requestedProjectName: string }
|
||||||
|
}) => {
|
||||||
|
return { message: '', name: '' }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.renameProject]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input: { context, requestedProjectName, projectName },
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
requestedProjectName: string
|
||||||
|
projectName: string
|
||||||
|
}
|
||||||
|
}): Promise<{ message: string; newName: string; oldName: string }> => {
|
||||||
|
return { message: '', newName: '', oldName: '' }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.createKCLFile]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
requestedProjectName: string
|
||||||
|
requestedFileName: string
|
||||||
|
requestedCode: string
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
}
|
||||||
|
}): Promise<{
|
||||||
|
message: string
|
||||||
|
fileName: string
|
||||||
|
projectName: string
|
||||||
|
}> => {
|
||||||
|
return { message: '', fileName: '', projectName: '' }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.checkReadWrite]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input: { context, requestedProjectDirectoryPath },
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
requestedProjectDirectoryPath: string
|
||||||
|
}
|
||||||
|
}): Promise<{ value: boolean; error: unknown }> => {
|
||||||
|
return { value: true, error: undefined }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
initial: SystemIOMachineStates.idle,
|
||||||
|
// Remember, this machine and change its projectDirectory at any point
|
||||||
|
// '' will be no project directory, aka clear this machine out!
|
||||||
|
// To be the absolute root of someones computer we should take the string of path.resolve() in node.js which is different for each OS
|
||||||
|
context: () => ({
|
||||||
|
folders: [],
|
||||||
|
defaultProjectFolderName: DEFAULT_PROJECT_NAME,
|
||||||
|
projectDirectoryPath: NO_PROJECT_DIRECTORY,
|
||||||
|
hasListedProjects: false,
|
||||||
|
requestedProjectName: { name: NO_PROJECT_DIRECTORY },
|
||||||
|
requestedFileName: {
|
||||||
|
project: NO_PROJECT_DIRECTORY,
|
||||||
|
file: NO_PROJECT_DIRECTORY,
|
||||||
|
},
|
||||||
|
canReadWriteProjectDirectory: { value: true, error: undefined },
|
||||||
|
clearURLParams: { value: false },
|
||||||
|
}),
|
||||||
|
states: {
|
||||||
|
[SystemIOMachineStates.idle]: {
|
||||||
|
on: {
|
||||||
|
// on can be an action
|
||||||
|
[SystemIOMachineEvents.readFoldersFromProjectDirectory]: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.setProjectDirectoryPath]: {
|
||||||
|
target: SystemIOMachineStates.checkingReadWrite,
|
||||||
|
actions: [SystemIOMachineActions.setProjectDirectoryPath],
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.navigateToProject]: {
|
||||||
|
actions: [SystemIOMachineActions.setRequestedProjectName],
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.navigateToFile]: {
|
||||||
|
actions: [SystemIOMachineActions.setRequestedFileName],
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.createProject]: {
|
||||||
|
target: SystemIOMachineStates.creatingProject,
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.renameProject]: {
|
||||||
|
target: SystemIOMachineStates.renamingProject,
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.deleteProject]: {
|
||||||
|
target: SystemIOMachineStates.deletingProject,
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.createKCLFile]: {
|
||||||
|
target: SystemIOMachineStates.creatingKCLFile,
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.setDefaultProjectFolderName]: {
|
||||||
|
actions: [SystemIOMachineActions.setDefaultProjectFolderName],
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.importFileFromURL]: {
|
||||||
|
target: SystemIOMachineStates.importFileFromURL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SystemIOMachineStates.readingFolders]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.readFoldersFromProjectDirectory,
|
||||||
|
src: SystemIOMachineActors.readFoldersFromProjectDirectory,
|
||||||
|
input: ({ context }) => {
|
||||||
|
return context
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
actions: [
|
||||||
|
SystemIOMachineActions.setFolders,
|
||||||
|
assign({ hasListedProjects: true }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SystemIOMachineStates.creatingProject]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.createProject,
|
||||||
|
src: SystemIOMachineActors.createProject,
|
||||||
|
input: ({ context, event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.createProject)
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
requestedProjectName: event.data.requestedProjectName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
requestedProjectName: ({ event }) => {
|
||||||
|
return { name: event.output.name }
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
SystemIOMachineActions.toastSuccess,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
actions: [SystemIOMachineActions.toastError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SystemIOMachineStates.renamingProject]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.renameProject,
|
||||||
|
src: SystemIOMachineActors.renameProject,
|
||||||
|
input: ({ context, event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.renameProject)
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
requestedProjectName: event.data.requestedProjectName,
|
||||||
|
projectName: event.data.projectName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
actions: [SystemIOMachineActions.toastSuccess],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
actions: [SystemIOMachineActions.toastError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SystemIOMachineStates.deletingProject]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.deleteProject,
|
||||||
|
src: SystemIOMachineActors.deleteProject,
|
||||||
|
input: ({ context, event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.deleteProject)
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
requestedProjectName: event.data.requestedProjectName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
actions: [SystemIOMachineActions.toastSuccess],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
actions: [SystemIOMachineActions.toastError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SystemIOMachineStates.creatingKCLFile]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.createKCLFile,
|
||||||
|
src: SystemIOMachineActors.createKCLFile,
|
||||||
|
input: ({ context, event, self }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.createKCLFile)
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
requestedProjectName: event.data.requestedProjectName,
|
||||||
|
requestedFileName: event.data.requestedFileName,
|
||||||
|
requestedCode: event.data.requestedCode,
|
||||||
|
rootContext: self.system.get('root').getSnapshot().context,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
actions: [SystemIOMachineActions.toastError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SystemIOMachineStates.importFileFromURL]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.importFileFromURL,
|
||||||
|
src: SystemIOMachineActors.createKCLFile,
|
||||||
|
input: ({ context, event, self }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.importFileFromURL)
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
requestedProjectName: event.data.requestedProjectName,
|
||||||
|
requestedFileName: event.data.requestedFileName,
|
||||||
|
requestedCode: event.data.requestedCode,
|
||||||
|
rootContext: self.system.get('root').getSnapshot().context,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
// Clear on web? not desktop
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
requestedFileName: ({ context, event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.done_importFileFromURL)
|
||||||
|
// Not the entire path
|
||||||
|
return {
|
||||||
|
project: event.output.projectName,
|
||||||
|
file: event.output.fileName + '.kcl',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
assign({ clearURLParams: { value: true } }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
actions: [SystemIOMachineActions.toastError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SystemIOMachineStates.checkingReadWrite]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.checkReadWrite,
|
||||||
|
src: SystemIOMachineActors.checkReadWrite,
|
||||||
|
input: ({ context, event }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.setProjectDirectoryPath)
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
requestedProjectDirectoryPath:
|
||||||
|
event.data.requestedProjectDirectoryPath,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
actions: [SystemIOMachineActions.toastError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watcher handler
|
||||||
|
// look at projectDirectory useEffect then send this event if it changes or if we need to do this?
|
||||||
|
// The handler needs to live somewhere... aka the provider?
|
233
src/machines/systemIO/systemIOMachineDesktop.ts
Normal file
233
src/machines/systemIO/systemIOMachineDesktop.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import {
|
||||||
|
createNewProjectDirectory,
|
||||||
|
getProjectInfo,
|
||||||
|
mkdirOrNOOP,
|
||||||
|
readAppSettingsFile,
|
||||||
|
renameProjectDirectory,
|
||||||
|
} from '@src/lib/desktop'
|
||||||
|
import {
|
||||||
|
doesProjectNameNeedInterpolated,
|
||||||
|
getNextFileName,
|
||||||
|
getNextProjectIndex,
|
||||||
|
getUniqueProjectName,
|
||||||
|
interpolateProjectNameWithIndex,
|
||||||
|
} from '@src/lib/desktopFS'
|
||||||
|
import type { Project } from '@src/lib/project'
|
||||||
|
import { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
||||||
|
import type { SystemIOContext } from '@src/machines/systemIO/utils'
|
||||||
|
import {
|
||||||
|
NO_PROJECT_DIRECTORY,
|
||||||
|
SystemIOMachineActors,
|
||||||
|
} from '@src/machines/systemIO/utils'
|
||||||
|
import { fromPromise } from 'xstate'
|
||||||
|
import type { AppMachineContext } from '@src/lib/types'
|
||||||
|
|
||||||
|
export const systemIOMachineDesktop = systemIOMachine.provide({
|
||||||
|
actors: {
|
||||||
|
[SystemIOMachineActors.readFoldersFromProjectDirectory]: fromPromise(
|
||||||
|
async ({ input: context }: { input: SystemIOContext }) => {
|
||||||
|
const projects = []
|
||||||
|
const projectDirectoryPath = context.projectDirectoryPath
|
||||||
|
if (projectDirectoryPath === NO_PROJECT_DIRECTORY) {
|
||||||
|
// TODO
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
await mkdirOrNOOP(projectDirectoryPath)
|
||||||
|
// Gotcha: readdir will list all folders at this project directory even if you do not have readwrite access on the directory path
|
||||||
|
const entries = await window.electron.readdir(projectDirectoryPath)
|
||||||
|
const { value: canReadWriteProjectDirectory } =
|
||||||
|
await window.electron.canReadWriteDirectory(projectDirectoryPath)
|
||||||
|
|
||||||
|
for (let entry of entries) {
|
||||||
|
// Skip directories that start with a dot
|
||||||
|
if (entry.startsWith('.')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const projectPath = window.electron.path.join(
|
||||||
|
projectDirectoryPath,
|
||||||
|
entry
|
||||||
|
)
|
||||||
|
|
||||||
|
// if it's not a directory ignore.
|
||||||
|
// Gotcha: statIsDirectory will work even if you do not have read write permissions on the project path
|
||||||
|
const isDirectory = await window.electron.statIsDirectory(projectPath)
|
||||||
|
if (!isDirectory) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const project: Project = await getProjectInfo(projectPath)
|
||||||
|
if (
|
||||||
|
project.kcl_file_count === 0 &&
|
||||||
|
project.readWriteAccess &&
|
||||||
|
canReadWriteProjectDirectory
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
projects.push(project)
|
||||||
|
}
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.createProject]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: { context: SystemIOContext; requestedProjectName: string }
|
||||||
|
}) => {
|
||||||
|
const folders = input.context.folders
|
||||||
|
const requestedProjectName = input.requestedProjectName
|
||||||
|
const uniqueName = getUniqueProjectName(requestedProjectName, folders)
|
||||||
|
await createNewProjectDirectory(uniqueName)
|
||||||
|
return {
|
||||||
|
message: `Successfully created "${uniqueName}"`,
|
||||||
|
name: uniqueName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.renameProject]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
requestedProjectName: string
|
||||||
|
projectName: string
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const folders = input.context.folders
|
||||||
|
const requestedProjectName = input.requestedProjectName
|
||||||
|
const projectName = input.projectName
|
||||||
|
let newProjectName: string = requestedProjectName
|
||||||
|
if (doesProjectNameNeedInterpolated(requestedProjectName)) {
|
||||||
|
const nextIndex = getNextProjectIndex(requestedProjectName, folders)
|
||||||
|
newProjectName = interpolateProjectNameWithIndex(
|
||||||
|
requestedProjectName,
|
||||||
|
nextIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast an error if the project name is taken
|
||||||
|
if (folders.find((p) => p.name === newProjectName)) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(`Project with name "${newProjectName}" already exists`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await renameProjectDirectory(
|
||||||
|
window.electron.path.join(
|
||||||
|
input.context.projectDirectoryPath,
|
||||||
|
projectName
|
||||||
|
),
|
||||||
|
newProjectName
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Successfully renamed "${projectName}" to "${newProjectName}"`,
|
||||||
|
oldName: projectName,
|
||||||
|
newName: newProjectName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.deleteProject]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: { context: SystemIOContext; requestedProjectName: string }
|
||||||
|
}) => {
|
||||||
|
await window.electron.rm(
|
||||||
|
window.electron.path.join(
|
||||||
|
input.context.projectDirectoryPath,
|
||||||
|
input.requestedProjectName
|
||||||
|
),
|
||||||
|
{
|
||||||
|
recursive: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Successfully deleted "${input.requestedProjectName}"`,
|
||||||
|
name: input.requestedProjectName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.createKCLFile]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
requestedProjectName: string
|
||||||
|
requestedFileName: string
|
||||||
|
requestedCode: string
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const requestedProjectName = input.requestedProjectName
|
||||||
|
const requestedFileName = input.requestedFileName
|
||||||
|
const requestedCode = input.requestedCode
|
||||||
|
const folders = input.context.folders
|
||||||
|
|
||||||
|
let newProjectName = requestedProjectName
|
||||||
|
|
||||||
|
if (!newProjectName) {
|
||||||
|
newProjectName = getUniqueProjectName(
|
||||||
|
input.context.defaultProjectFolderName,
|
||||||
|
input.context.folders
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsInterpolated =
|
||||||
|
doesProjectNameNeedInterpolated(newProjectName)
|
||||||
|
if (needsInterpolated) {
|
||||||
|
const nextIndex = getNextProjectIndex(newProjectName, folders)
|
||||||
|
newProjectName = interpolateProjectNameWithIndex(
|
||||||
|
newProjectName,
|
||||||
|
nextIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseDir = window.electron.join(
|
||||||
|
input.context.projectDirectoryPath,
|
||||||
|
newProjectName
|
||||||
|
)
|
||||||
|
const { name: newFileName } = getNextFileName({
|
||||||
|
entryName: requestedFileName,
|
||||||
|
baseDir,
|
||||||
|
})
|
||||||
|
const configuration = await readAppSettingsFile()
|
||||||
|
|
||||||
|
// Create the project around the file if newProject
|
||||||
|
await createNewProjectDirectory(
|
||||||
|
newProjectName,
|
||||||
|
requestedCode,
|
||||||
|
configuration,
|
||||||
|
newFileName
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'File created successfully',
|
||||||
|
fileName: input.requestedFileName,
|
||||||
|
projectName: newProjectName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.checkReadWrite]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
requestedProjectDirectoryPath: string
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const requestProjectDirectoryPath = input.requestedProjectDirectoryPath
|
||||||
|
if (!requestProjectDirectoryPath) {
|
||||||
|
return { value: true, error: undefined }
|
||||||
|
}
|
||||||
|
const result = await window.electron.canReadWriteDirectory(
|
||||||
|
requestProjectDirectoryPath
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
50
src/machines/systemIO/systemIOMachineWeb.ts
Normal file
50
src/machines/systemIO/systemIOMachineWeb.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
||||||
|
import type { SystemIOContext } from '@src/machines/systemIO/utils'
|
||||||
|
import { SystemIOMachineActors } from '@src/machines/systemIO/utils'
|
||||||
|
import { fromPromise } from 'xstate'
|
||||||
|
import { newKclFile } from '@src/lang/project'
|
||||||
|
import { readLocalStorageProjectSettingsFile } from '@src/lib/settings/settingsUtils'
|
||||||
|
import { err } from '@src/lib/trap'
|
||||||
|
import { DEFAULT_DEFAULT_LENGTH_UNIT } from '@src/lib/constants'
|
||||||
|
import type { AppMachineContext } from '@src/lib/types'
|
||||||
|
|
||||||
|
export const systemIOMachineWeb = systemIOMachine.provide({
|
||||||
|
actors: {
|
||||||
|
[SystemIOMachineActors.createKCLFile]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
requestedProjectName: string
|
||||||
|
requestedFileName: string
|
||||||
|
requestedCode: string
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
// Browser version doesn't navigate, just overwrites the current file
|
||||||
|
// clearImportSearchParams()
|
||||||
|
const projectSettings = readLocalStorageProjectSettingsFile()
|
||||||
|
if (err(projectSettings)) {
|
||||||
|
return Promise.reject(
|
||||||
|
'Unable to read project settings from local storage'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const codeToWrite = newKclFile(
|
||||||
|
input.requestedCode,
|
||||||
|
projectSettings?.settings?.modeling?.base_unit ||
|
||||||
|
DEFAULT_DEFAULT_LENGTH_UNIT
|
||||||
|
)
|
||||||
|
if (err(codeToWrite)) return Promise.reject(codeToWrite)
|
||||||
|
input.rootContext.codeManager.updateCodeStateEditor(codeToWrite)
|
||||||
|
await input.rootContext.codeManager.writeToFile()
|
||||||
|
await input.rootContext.kclManager.executeCode()
|
||||||
|
return {
|
||||||
|
message: 'File overwritten successfully',
|
||||||
|
fileName: input.requestedFileName,
|
||||||
|
projectName: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
73
src/machines/systemIO/utils.ts
Normal file
73
src/machines/systemIO/utils.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import type { Project } from '@src/lib/project'
|
||||||
|
|
||||||
|
export enum SystemIOMachineActors {
|
||||||
|
readFoldersFromProjectDirectory = 'read folders from project directory',
|
||||||
|
setProjectDirectoryPath = 'set project directory path',
|
||||||
|
createProject = 'create project',
|
||||||
|
renameProject = 'rename project',
|
||||||
|
deleteProject = 'delete project',
|
||||||
|
createKCLFile = 'create kcl file',
|
||||||
|
checkReadWrite = 'check read write',
|
||||||
|
importFileFromURL = 'import file from URL',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SystemIOMachineStates {
|
||||||
|
idle = 'idle',
|
||||||
|
readingFolders = 'readingFolders',
|
||||||
|
settingProjectDirectoryPath = 'settingProjectDirectoryPath',
|
||||||
|
creatingProject = 'creatingProject',
|
||||||
|
renamingProject = 'renamingProject',
|
||||||
|
deletingProject = 'deletingProject',
|
||||||
|
creatingKCLFile = 'creatingKCLFile',
|
||||||
|
checkingReadWrite = 'checkingReadWrite',
|
||||||
|
importFileFromURL = 'importFileFromURL',
|
||||||
|
}
|
||||||
|
|
||||||
|
const donePrefix = 'xstate.done.actor.'
|
||||||
|
|
||||||
|
export enum SystemIOMachineEvents {
|
||||||
|
readFoldersFromProjectDirectory = 'read folders from project directory',
|
||||||
|
done_readFoldersFromProjectDirectory = donePrefix +
|
||||||
|
'read folders from project directory',
|
||||||
|
setProjectDirectoryPath = 'set project directory path',
|
||||||
|
navigateToProject = 'navigate to project',
|
||||||
|
navigateToFile = 'navigate to file',
|
||||||
|
createProject = 'create project',
|
||||||
|
renameProject = 'rename project',
|
||||||
|
deleteProject = 'delete project',
|
||||||
|
createKCLFile = 'create kcl file',
|
||||||
|
setDefaultProjectFolderName = 'set default project folder name',
|
||||||
|
done_checkReadWrite = donePrefix + 'check read write',
|
||||||
|
importFileFromURL = 'import file from URL',
|
||||||
|
done_importFileFromURL = donePrefix + 'import file from URL',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SystemIOMachineActions {
|
||||||
|
setFolders = 'set folders',
|
||||||
|
setProjectDirectoryPath = 'set project directory path',
|
||||||
|
setRequestedProjectName = 'set requested project name',
|
||||||
|
setRequestedFileName = 'set requested file name',
|
||||||
|
setDefaultProjectFolderName = 'set default project folder name',
|
||||||
|
toastSuccess = 'toastSuccess',
|
||||||
|
toastError = 'toastError',
|
||||||
|
setReadWriteProjectDirectory = 'set read write project directory',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NO_PROJECT_DIRECTORY = ''
|
||||||
|
|
||||||
|
export type SystemIOContext = {
|
||||||
|
// Only store folders under the projectDirectory, do not maintain folders outside this directory
|
||||||
|
folders: Project[]
|
||||||
|
// For this machines runtime, this is the default string when creating a project
|
||||||
|
// A project is defined by creating a folder at the one level below the working project directory
|
||||||
|
defaultProjectFolderName: string
|
||||||
|
// working project directory that stores all the project folders
|
||||||
|
projectDirectoryPath: string
|
||||||
|
// has the application gone through the initialization of systemIOMachine at least once.
|
||||||
|
// this is required to prevent chokidar from spamming invalid events during initialization.
|
||||||
|
hasListedProjects: boolean
|
||||||
|
requestedProjectName: { name: string }
|
||||||
|
requestedFileName: { project: string; file: string }
|
||||||
|
canReadWriteProjectDirectory: { value: boolean; error: unknown }
|
||||||
|
clearURLParams: { value: boolean }
|
||||||
|
}
|
@ -10,7 +10,7 @@ import {
|
|||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { uuidv4 } from '@src/lib/utils'
|
import { uuidv4 } from '@src/lib/utils'
|
||||||
import { authActor, settingsActor } from '@src/machines/appMachine'
|
import { authActor, settingsActor } from '@src/lib/singletons'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
import type { WebContentSendPayload } from '@src/menu/channels'
|
||||||
import type { NavigateFunction } from 'react-router-dom'
|
import type { NavigateFunction } from 'react-router-dom'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
@ -15,7 +15,6 @@ import {
|
|||||||
} from '@src/components/ProjectSearchBar'
|
} from '@src/components/ProjectSearchBar'
|
||||||
import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher'
|
import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher'
|
||||||
import { useMenuListener } from '@src/hooks/useMenu'
|
import { useMenuListener } from '@src/hooks/useMenu'
|
||||||
import { useProjectsContext } from '@src/hooks/useProjectsContext'
|
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { markOnce } from '@src/lib/performance'
|
import { markOnce } from '@src/lib/performance'
|
||||||
@ -27,21 +26,24 @@ import {
|
|||||||
getSortIcon,
|
getSortIcon,
|
||||||
} from '@src/lib/sorting'
|
} from '@src/lib/sorting'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { authActor, useSettings } from '@src/machines/appMachine'
|
import { authActor, systemIOActor, useSettings } from '@src/lib/singletons'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
|
import {
|
||||||
|
useCanReadWriteProjectDirectory,
|
||||||
|
useFolders,
|
||||||
|
useState as useSystemIOState,
|
||||||
|
} from '@src/machines/systemIO/hooks'
|
||||||
|
import {
|
||||||
|
SystemIOMachineEvents,
|
||||||
|
SystemIOMachineStates,
|
||||||
|
} from '@src/machines/systemIO/utils'
|
||||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
import type { WebContentSendPayload } from '@src/menu/channels'
|
||||||
|
|
||||||
// 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.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { state, send } = useProjectsContext()
|
const state = useSystemIOState()
|
||||||
const [readWriteProjectDir, setReadWriteProjectDir] = useState<{
|
const readWriteProjectDir = useCanReadWriteProjectDirectory()
|
||||||
value: boolean
|
|
||||||
error: unknown
|
|
||||||
}>({
|
|
||||||
value: true,
|
|
||||||
error: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Only create the native file menus on desktop
|
// Only create the native file menus on desktop
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -156,40 +158,13 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const projects = useFolders()
|
||||||
const projects = state?.context.projects ?? []
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
||||||
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
||||||
|
|
||||||
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
||||||
|
|
||||||
// Update the default project name and directory in the home machine
|
|
||||||
// when the settings change
|
|
||||||
useEffect(() => {
|
|
||||||
send({
|
|
||||||
type: 'assign',
|
|
||||||
data: {
|
|
||||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
|
||||||
defaultDirectory: settings.app.projectDirectory.current,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Must be a truthy string, not '' or null or undefined
|
|
||||||
if (settings.app.projectDirectory.current) {
|
|
||||||
window.electron
|
|
||||||
.canReadWriteDirectory(settings.app.projectDirectory.current)
|
|
||||||
.then((res) => {
|
|
||||||
setReadWriteProjectDir(res)
|
|
||||||
})
|
|
||||||
.catch(reportRejection)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
settings.app.projectDirectory.current,
|
|
||||||
settings.projects.defaultProjectName.current,
|
|
||||||
send,
|
|
||||||
])
|
|
||||||
|
|
||||||
async function handleRenameProject(
|
async function handleRenameProject(
|
||||||
e: FormEvent<HTMLFormElement>,
|
e: FormEvent<HTMLFormElement>,
|
||||||
project: Project
|
project: Project
|
||||||
@ -204,17 +179,20 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newProjectName !== project.name) {
|
if (newProjectName !== project.name) {
|
||||||
send({
|
systemIOActor.send({
|
||||||
type: 'Rename project',
|
type: SystemIOMachineEvents.renameProject,
|
||||||
data: { oldName: project.name, newName: newProjectName as string },
|
data: {
|
||||||
|
requestedProjectName: String(newProjectName),
|
||||||
|
projectName: project.name,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteProject(project: Project) {
|
async function handleDeleteProject(project: Project) {
|
||||||
send({
|
systemIOActor.send({
|
||||||
type: 'Delete project',
|
type: SystemIOMachineEvents.deleteProject,
|
||||||
data: { name: project.name || '' },
|
data: { requestedProjectName: project.name },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/** Type narrowing function of unknown error to a string */
|
/** Type narrowing function of unknown error to a string */
|
||||||
@ -246,9 +224,6 @@ const Home = () => {
|
|||||||
data: {
|
data: {
|
||||||
groupId: 'projects',
|
groupId: 'projects',
|
||||||
name: 'Create project',
|
name: 'Create project',
|
||||||
argDefaultValues: {
|
|
||||||
name: settings.projects.defaultProjectName.current,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -345,7 +320,7 @@ const Home = () => {
|
|||||||
data-testid="home-section"
|
data-testid="home-section"
|
||||||
className="flex-1 overflow-y-auto pr-2 pb-24"
|
className="flex-1 overflow-y-auto pr-2 pb-24"
|
||||||
>
|
>
|
||||||
{state?.matches('Reading projects') ? (
|
{state?.matches(SystemIOMachineStates.readingFolders) ? (
|
||||||
<Loading>Loading your Projects...</Loading>
|
<Loading>Loading your Projects...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SettingsSection } from '@src/components/Settings/SettingsSection'
|
import { SettingsSection } from '@src/components/Settings/SettingsSection'
|
||||||
import type { CameraSystem } from '@src/lib/cameraControls'
|
import type { CameraSystem } from '@src/lib/cameraControls'
|
||||||
import { cameraMouseDragGuards, cameraSystems } from '@src/lib/cameraControls'
|
import { cameraMouseDragGuards, cameraSystems } from '@src/lib/cameraControls'
|
||||||
import { settingsActor, useSettings } from '@src/machines/appMachine'
|
import { settingsActor, useSettings } from '@src/lib/singletons'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -13,7 +13,7 @@ import { codeManager, kclManager } from '@src/lib/singletons'
|
|||||||
import { Themes, getSystemTheme } from '@src/lib/theme'
|
import { Themes, getSystemTheme } from '@src/lib/theme'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
|
||||||
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
|
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { bracketThicknessCalculationLine } from '@src/lib/exampleKcl'
|
import { bracketThicknessCalculationLine } from '@src/lib/exampleKcl'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { Themes, getSystemTheme } from '@src/lib/theme'
|
import { Themes, getSystemTheme } from '@src/lib/theme'
|
||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
|
||||||
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
|
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
|
||||||
|
@ -3,7 +3,7 @@ import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
|||||||
import { ActionButton } from '@src/components/ActionButton'
|
import { ActionButton } from '@src/components/ActionButton'
|
||||||
import { SettingsSection } from '@src/components/Settings/SettingsSection'
|
import { SettingsSection } from '@src/components/Settings/SettingsSection'
|
||||||
import { type BaseUnit, baseUnitsUnion } from '@src/lib/settings/settingsTypes'
|
import { type BaseUnit, baseUnitsUnion } from '@src/lib/settings/settingsTypes'
|
||||||
import { settingsActor, useSettings } from '@src/machines/appMachine'
|
import { settingsActor, useSettings } from '@src/lib/singletons'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
|
||||||
import { useDismiss, useNextClick } from '@src/routes/Onboarding/utils'
|
import { useDismiss, useNextClick } from '@src/routes/Onboarding/utils'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { useUser } from '@src/machines/appMachine'
|
import { useUser } from '@src/lib/singletons'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
|
||||||
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
|
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
|
||||||
|
@ -14,7 +14,7 @@ import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
|
|||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
||||||
import { reportRejection, trap } from '@src/lib/trap'
|
import { reportRejection, trap } from '@src/lib/trap'
|
||||||
import { settingsActor } from '@src/machines/appMachine'
|
import { settingsActor } from '@src/lib/singletons'
|
||||||
import { onboardingRoutes } from '@src/routes/Onboarding'
|
import { onboardingRoutes } from '@src/routes/Onboarding'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
import { parse, resultIsOk } from '@src/lang/wasm'
|
import { parse, resultIsOk } from '@src/lang/wasm'
|
||||||
|
@ -14,7 +14,7 @@ import { PATHS } from '@src/lib/paths'
|
|||||||
import { Themes, getSystemTheme } from '@src/lib/theme'
|
import { Themes, getSystemTheme } from '@src/lib/theme'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { toSync } from '@src/lib/utils'
|
import { toSync } from '@src/lib/utils'
|
||||||
import { authActor, useSettings } from '@src/machines/appMachine'
|
import { authActor, useSettings } from '@src/lib/singletons'
|
||||||
import { APP_VERSION, IS_NIGHTLY } from '@src/routes/utils'
|
import { APP_VERSION, IS_NIGHTLY } from '@src/routes/utils'
|
||||||
|
|
||||||
const subtleBorder =
|
const subtleBorder =
|
||||||
|
Reference in New Issue
Block a user