Merge remote-tracking branch 'origin' into kurt-circle

This commit is contained in:
Kurt Hutten Irev-Dev
2024-09-10 11:34:42 +10:00
83 changed files with 4456 additions and 3632 deletions

View File

@ -13,6 +13,8 @@
"plugin:css-modules/recommended" "plugin:css-modules/recommended"
], ],
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"semi": [ "semi": [
"error", "error",
"never" "never"
@ -24,7 +26,6 @@
{ {
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "warn",
"suggest-no-throw/suggest-no-throw": "off", "suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off", "testing-library/prefer-screen-queries": "off",
"jest/valid-expect": "off" "jest/valid-expect": "off"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -27,6 +27,7 @@ import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants' import { SETTINGS_FILE_NAME } from 'lib/constants'
import { isArray } from 'lib/utils' import { isArray } from 'lib/utils'
import { reportRejection } from 'lib/trap'
type TestColor = [number, number, number] type TestColor = [number, number, number]
export const TEST_COLORS = { export const TEST_COLORS = {
@ -439,46 +440,50 @@ export async function getUtils(page: Page, test_?: typeof test) {
} }
return maxDiff return maxDiff
}, },
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) => doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
new Promise(async (resolve) => { new Promise<boolean>((resolve) => {
await page.screenshot({ ;(async () => {
path: './e2e/playwright/temp1.png',
fullPage: true,
})
await fn()
const isImageDiff = async () => {
await page.screenshot({ await page.screenshot({
path: './e2e/playwright/temp2.png', path: './e2e/playwright/temp1.png',
fullPage: true, fullPage: true,
}) })
const screenshot1 = PNG.sync.read( await fn()
await fsp.readFile('./e2e/playwright/temp1.png') const isImageDiff = async () => {
) await page.screenshot({
const screenshot2 = PNG.sync.read( path: './e2e/playwright/temp2.png',
await fsp.readFile('./e2e/playwright/temp2.png') fullPage: true,
) })
const actualDiffCount = pixelMatch( const screenshot1 = PNG.sync.read(
screenshot1.data, await fsp.readFile('./e2e/playwright/temp1.png')
screenshot2.data, )
null, const screenshot2 = PNG.sync.read(
screenshot1.width, await fsp.readFile('./e2e/playwright/temp2.png')
screenshot2.height )
) const actualDiffCount = pixelMatch(
return actualDiffCount > diffCount screenshot1.data,
} screenshot2.data,
null,
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times) screenshot1.width,
let count = 0 screenshot2.height
const interval = setInterval(async () => { )
count++ return actualDiffCount > diffCount
if (await isImageDiff()) {
clearInterval(interval)
resolve(true)
} else if (count > 100) {
clearInterval(interval)
resolve(false)
} }
}, 50)
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
let count = 0
const interval = setInterval(() => {
;(async () => {
count++
if (await isImageDiff()) {
clearInterval(interval)
resolve(true)
} else if (count > 100) {
clearInterval(interval)
resolve(false)
}
})().catch(reportRejection)
}, 50)
})().catch(reportRejection)
}), }),
emulateNetworkConditions: async ( emulateNetworkConditions: async (
networkOptions: Protocol.Network.emulateNetworkConditionsParameters networkOptions: Protocol.Network.emulateNetworkConditionsParameters

View File

@ -34,7 +34,7 @@
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1", "@tweenjs/tween.js": "^23.1.1",
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2", "@xstate/react": "^4.1.1",
"bonjour-service": "^1.2.1", "bonjour-service": "^1.2.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
@ -64,7 +64,7 @@
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8", "vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2", "web-vitals": "^3.5.2",
"xstate": "^4.38.2" "xstate": "^5.17.4"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",

View File

@ -72,6 +72,7 @@ export class LanguageServerClient {
async initialize() { async initialize() {
// Start the client in the background. // Start the client in the background.
this.client.setNotifyFn(this.processNotifications.bind(this)) this.client.setNotifyFn(this.processNotifications.bind(this))
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.client.start() this.client.start()
this.ready = true this.ready = true
@ -195,6 +196,9 @@ export class LanguageServerClient {
} }
private processNotifications(notification: LSP.NotificationMessage) { private processNotifications(notification: LSP.NotificationMessage) {
for (const plugin of this.plugins) plugin.processNotification(notification) for (const plugin of this.plugins) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
plugin.processNotification(notification)
}
} }
} }

View File

@ -12,6 +12,7 @@ export default function lspFormatExt(
run: (view: EditorView) => { run: (view: EditorView) => {
let value = view.plugin(plugin) let value = view.plugin(plugin)
if (!value) return false if (!value) return false
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.requestFormatting() value.requestFormatting()
return true return true
}, },

View File

@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
this.processLspNotification = options.processLspNotification this.processLspNotification = options.processLspNotification
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.initialize({ this.initialize({
documentText: this.getDocText(), documentText: this.getDocText(),
}) })
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
async initialize({ documentText }: { documentText: string }) { async initialize({ documentText }: { documentText: string }) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (this.client.initializePromise) { if (this.client.initializePromise) {
await this.client.initializePromise await this.client.initializePromise
} }
@ -162,7 +164,9 @@ export class LanguageServerPlugin implements PluginValue {
}, },
}) })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.requestSemanticTokens() this.requestSemanticTokens()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateFoldingRanges() this.updateFoldingRanges()
} }
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
contentChanges: [{ text: this.view.state.doc.toString() }], contentChanges: [{ text: this.view.state.doc.toString() }],
}) })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.requestSemanticTokens() this.requestSemanticTokens()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateFoldingRanges() this.updateFoldingRanges()
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View File

@ -26,6 +26,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
import Gizmo from 'components/Gizmo' import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu' import { UnitsMenu } from 'components/UnitsMenu'
import { reportRejection } from 'lib/trap'
export function App() { export function App() {
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
@ -80,7 +81,7 @@ export function App() {
useEngineConnectionSubscriptions() useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => { const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager.sendSceneCommand(message) engineCommandManager.sendSceneCommand(message).catch(reportRejection)
}, 1000 / 15) }, 1000 / 15)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
if (state.matches('Sketch')) { if (state.matches('Sketch')) {
@ -95,7 +96,7 @@ export function App() {
}) })
const newCmdId = uuidv4() const newCmdId = uuidv4()
if (state.matches('idle.showPlanes')) return if (state.matches({ idle: 'showPlanes' })) return
if (context.store?.buttonDownInStream !== undefined) return if (context.store?.buttonDownInStream !== undefined) return
debounceSocketSend({ debounceSocketSend({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',

View File

@ -41,6 +41,7 @@ import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
import { useMemo } from 'react' import { useMemo } from 'react'
import { AppStateProvider } from 'AppState' import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -173,21 +174,23 @@ function CoreDump() {
[] []
) )
useHotkeyWrapper(['mod + shift + .'], () => { useHotkeyWrapper(['mod + shift + .'], () => {
toast.promise( toast
coreDump(coreDumpManager, true), .promise(
{ coreDump(coreDumpManager, true),
loading: 'Starting core dump...', {
success: 'Core dump completed successfully', loading: 'Starting core dump...',
error: 'Error while exporting core dump', success: 'Core dump completed successfully',
}, error: 'Error while exporting core dump',
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
}, },
} {
) success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
.catch(reportRejection)
}) })
return null return null
} }

View File

@ -70,12 +70,12 @@ export function Toolbar({
*/ */
const configCallbackProps: ToolbarItemCallbackProps = useMemo( const configCallbackProps: ToolbarItemCallbackProps = useMemo(
() => ({ () => ({
modelingStateMatches: state.matches, modelingState: state,
modelingSend: send, modelingSend: send,
commandBarSend, commandBarSend,
sketchPathId, sketchPathId,
}), }),
[state.matches, send, commandBarSend, sketchPathId] [state, send, commandBarSend, sketchPathId]
) )
/** /**

View File

@ -22,11 +22,12 @@ import {
UnreliableSubscription, UnreliableSubscription,
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils' import { toSync, uuidv4 } from 'lib/utils'
import { deg2Rad } from 'lib/utils2d' import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils' import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'
import { isQuaternionVertical } from './helpers' import { isQuaternionVertical } from './helpers'
import { reportRejection } from 'lib/trap'
const ORTHOGRAPHIC_CAMERA_SIZE = 20 const ORTHOGRAPHIC_CAMERA_SIZE = 20
const FRAMES_TO_ANIMATE_IN = 30 const FRAMES_TO_ANIMATE_IN = 30
@ -100,6 +101,7 @@ export class CameraControls {
camProps.type === 'perspective' && camProps.type === 'perspective' &&
this.camera instanceof OrthographicCamera this.camera instanceof OrthographicCamera
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
} else if ( } else if (
camProps.type === 'orthographic' && camProps.type === 'orthographic' &&
@ -127,6 +129,7 @@ export class CameraControls {
} }
throttledEngCmd = throttle((cmd: EngineCommand) => { throttledEngCmd = throttle((cmd: EngineCommand) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(cmd) this.engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 30) }, 1000 / 30)
@ -139,6 +142,7 @@ export class CameraControls {
...convertThreeCamValuesToEngineCam(threeValues), ...convertThreeCamValuesToEngineCam(threeValues),
}, },
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(cmd) this.engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 15) }, 1000 / 15)
@ -151,6 +155,7 @@ export class CameraControls {
this.lastPerspectiveCmd && this.lastPerspectiveCmd &&
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true) this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
this.lastPerspectiveCmdTime = Date.now() this.lastPerspectiveCmdTime = Date.now()
} }
@ -218,6 +223,7 @@ export class CameraControls {
this.useOrthographicCamera() this.useOrthographicCamera()
} }
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) { if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
} }
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) { if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
@ -249,6 +255,7 @@ export class CameraControls {
const doZoom = () => { const doZoom = () => {
if (this.zoomDataFromLastFrame !== undefined) { if (this.zoomDataFromLastFrame !== undefined) {
this.handleStart() this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -266,6 +273,7 @@ export class CameraControls {
const doMove = () => { const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) { if (this.moveDataFromLastFrame !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -459,6 +467,7 @@ export class CameraControls {
this.camera.quaternion.set(qx, qy, qz, qw) this.camera.quaternion.set(qx, qy, qz, qw)
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -929,6 +938,7 @@ export class CameraControls {
} }
if (isReducedMotion()) { if (isReducedMotion()) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
onComplete() onComplete()
return return
} }
@ -937,7 +947,7 @@ export class CameraControls {
.to({ t: tweenEnd }, duration) .to({ t: tweenEnd }, duration)
.easing(TWEEN.Easing.Quadratic.InOut) .easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ t }) => cameraAtTime(t)) .onUpdate(({ t }) => cameraAtTime(t))
.onComplete(onComplete) .onComplete(toSync(onComplete, reportRejection))
.start() .start()
}) })
} }
@ -962,6 +972,7 @@ export class CameraControls {
// Decrease the FOV // Decrease the FOV
currentFov = Math.max(currentFov - fovAnimationStep, targetFov) currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dollyZoom(currentFov) this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation requestAnimationFrame(animateFovChange) // Continue the animation
} else if (frameWaitOnFinish > 0) { } else if (frameWaitOnFinish > 0) {
@ -991,6 +1002,7 @@ export class CameraControls {
this.lastPerspectiveFov = 4 this.lastPerspectiveFov = 4
let currentFov = 4 let currentFov = 4
const initialCameraUp = this.camera.up.clone() const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
const tempVec = new Vector3() const tempVec = new Vector3()
@ -999,6 +1011,7 @@ export class CameraControls {
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t) const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
this.camera.up.copy(currentUp) this.camera.up.copy(currentUp)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dollyZoom(currentFov) this.dollyZoom(currentFov)
} }
@ -1027,6 +1040,7 @@ export class CameraControls {
this.lastPerspectiveFov = 4 this.lastPerspectiveFov = 4
let currentFov = 4 let currentFov = 4
const initialCameraUp = this.camera.up.clone() const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
const tempVec = new Vector3() const tempVec = new Vector3()

View File

@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls' import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils' import { throttle, toSync } from 'lib/utils'
import { import {
sceneInfra, sceneInfra,
kclManager, kclManager,
@ -43,7 +43,7 @@ import {
removeSingleConstraintInfo, removeSingleConstraintInfo,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { err, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false) const [isCamMoving, setIsCamMoving] = useState(false)
@ -123,10 +123,10 @@ export const ClientSideScene = ({
} else if (context.mouseState.type === 'isDragging') { } else if (context.mouseState.type === 'isDragging') {
cursor = 'grabbing' cursor = 'grabbing'
} else if ( } else if (
state.matches('Sketch.Line tool') || state.matches({ Sketch: 'Line tool' }) ||
state.matches('Sketch.Tangential arc to') || state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches('Sketch.Rectangle tool') || state.matches({ Sketch: 'Rectangle tool' }) ||
state.matches('Sketch.Circle tool') state.matches({ Sketch: 'Circle tool' })
) { ) {
cursor = 'crosshair' cursor = 'crosshair'
} else { } else {
@ -214,9 +214,9 @@ const Overlay = ({
overlay.visible && overlay.visible &&
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' && typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
!( !(
state.matches('Sketch.Line tool') || state.matches({ Sketch: 'Line tool' }) ||
state.matches('Sketch.Tangential arc to') || state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches('Sketch.Rectangle tool') state.matches({ Sketch: 'Rectangle tool' })
) )
return ( return (
@ -587,7 +587,7 @@ const ConstraintSymbol = ({
}} }}
// disabled={isConstrained || !convertToVarEnabled} // disabled={isConstrained || !convertToVarEnabled}
// disabled={implicitDesc} TODO why does this change styles that are hard to override? // disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={async () => { onClick={toSync(async () => {
if (!isConstrained) { if (!isConstrained) {
send({ send({
type: 'Convert to variable', type: 'Convert to variable',
@ -618,13 +618,14 @@ const ConstraintSymbol = ({
) )
if (!transform) return if (!transform) return
const { modifiedAst } = transform const { modifiedAst } = transform
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.updateAst(modifiedAst, true) kclManager.updateAst(modifiedAst, true)
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)
} }
toast.success('Constraint removed') toast.success('Constraint removed')
} }
}} }, reportRejection)}
> >
<CustomIcon name={name} /> <CustomIcon name={name} />
</button> </button>
@ -690,7 +691,7 @@ const ConstraintSymbol = ({
const throttled = throttle((a: ReactCameraProperties) => { const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) { if (a.type === 'perspective' && a.fov) {
sceneInfra.camControls.dollyZoom(a.fov) sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection)
} }
}, 1000 / 15) }, 1000 / 15)
@ -720,6 +721,7 @@ export const CamDebugSettings = () => {
if (camSettings.type === 'perspective') { if (camSettings.type === 'perspective') {
sceneInfra.camControls.useOrthographicCamera() sceneInfra.camControls.useOrthographicCamera()
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneInfra.camControls.usePerspectiveCamera(true) sceneInfra.camControls.usePerspectiveCamera(true)
} }
}} }}
@ -727,7 +729,7 @@ export const CamDebugSettings = () => {
<div> <div>
<button <button
onClick={() => { onClick={() => {
sceneInfra.camControls.resetCameraPosition() sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}} }}
> >
Reset Camera Position Reset Camera Position

View File

@ -106,7 +106,7 @@ import {
updateRectangleSketch, updateRectangleSketch,
} from 'lib/rectangleTool' } from 'lib/rectangleTool'
import { getThemeColorForThreeJs } from 'lib/theme' import { getThemeColorForThreeJs } from 'lib/theme'
import { err, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d' import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
@ -351,6 +351,7 @@ export class SceneEntities {
) )
} }
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
@ -692,6 +693,7 @@ export class SceneEntities {
draftExpressionsIndices, draftExpressionsIndices,
}) })
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
@ -762,7 +764,7 @@ export class SceneEntities {
if (profileStart) { if (profileStart) {
sceneInfra.modelingSend({ type: 'CancelSketch' }) sceneInfra.modelingSend({ type: 'CancelSketch' })
} else { } else {
this.setUpDraftSegment( await this.setUpDraftSegment(
sketchPathToNode, sketchPathToNode,
forward, forward,
up, up,
@ -837,6 +839,7 @@ export class SceneEntities {
}) })
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onMove: async (args) => { onMove: async (args) => {
// Update the width and height of the draft rectangle // Update the width and height of the draft rectangle
const pathToNodeTwo = structuredClone(sketchPathToNode) const pathToNodeTwo = structuredClone(sketchPathToNode)
@ -884,6 +887,7 @@ export class SceneEntities {
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup) this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
) )
}, },
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => { onClick: async (args) => {
// Commit the rectangle to the full AST/code and return to sketch.idle // Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD const cornerPoint = args.intersectionPoint?.twoD
@ -1103,9 +1107,11 @@ export class SceneEntities {
}) => { }) => {
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing' let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onDragEnd: async () => { onDragEnd: async () => {
if (addingNewSegmentStatus !== 'nothing') { if (addingNewSegmentStatus !== 'nothing') {
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupSketch({ this.setupSketch({
sketchPathToNode: pathToNode, sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast, maybeModdedAst: kclManager.ast,
@ -1122,6 +1128,7 @@ export class SceneEntities {
}) })
} }
}, },
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onDrag: async ({ onDrag: async ({
selected, selected,
intersectionPoint, intersectionPoint,
@ -1172,6 +1179,7 @@ export class SceneEntities {
await kclManager.executeAstMock(mod.modifiedAst) await kclManager.executeAstMock(mod.modifiedAst)
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupSketch({ this.setupSketch({
sketchPathToNode: pathToNode, sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast, maybeModdedAst: kclManager.ast,
@ -1420,7 +1428,7 @@ export class SceneEntities {
) )
) )
sceneInfra.overlayCallbacks(callBacks) sceneInfra.overlayCallbacks(callBacks)
})() })().catch(reportRejection)
} }
/** /**

View File

@ -111,21 +111,21 @@ export class SceneInfra {
} }
extraSegmentTexture: Texture extraSegmentTexture: Texture
lastMouseState: MouseState = { type: 'idle' } lastMouseState: MouseState = { type: 'idle' }
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {} onDragStartCallback: (arg: OnDragCallbackArgs) => any = () => {}
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {} onDragEndCallback: (arg: OnDragCallbackArgs) => any = () => {}
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {} onDragCallback: (arg: OnDragCallbackArgs) => any = () => {}
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {} onMoveCallback: (arg: OnMoveCallbackArgs) => any = () => {}
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {} onClickCallback: (arg: OnClickCallbackArgs) => any = () => {}
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {} onMouseEnter: (arg: OnMouseEnterLeaveArgs) => any = () => {}
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {} onMouseLeave: (arg: OnMouseEnterLeaveArgs) => any = () => {}
setCallbacks = (callbacks: { setCallbacks = (callbacks: {
onDragStart?: (arg: OnDragCallbackArgs) => void onDragStart?: (arg: OnDragCallbackArgs) => any
onDragEnd?: (arg: OnDragCallbackArgs) => void onDragEnd?: (arg: OnDragCallbackArgs) => any
onDrag?: (arg: OnDragCallbackArgs) => void onDrag?: (arg: OnDragCallbackArgs) => any
onMove?: (arg: OnMoveCallbackArgs) => void onMove?: (arg: OnMoveCallbackArgs) => any
onClick?: (arg: OnClickCallbackArgs) => void onClick?: (arg: OnClickCallbackArgs) => any
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => any
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => any
}) => { }) => {
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback

View File

@ -151,6 +151,7 @@ export function useCalc({
}) })
if (trap(error)) return if (trap(error)) return
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { EngineCommandManagerEvents } from 'lang/std/engineConnection' import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
import { engineCommandManager, sceneInfra } from 'lib/singletons' import { engineCommandManager, sceneInfra } from 'lib/singletons'
import { throttle, isReducedMotion } from 'lib/utils' import { throttle, isReducedMotion } from 'lib/utils'
import { reportRejection } from 'lib/trap'
const updateDollyZoom = throttle( const updateDollyZoom = throttle(
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov), (newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
@ -16,8 +17,8 @@ export const CamToggle = () => {
useEffect(() => { useEffect(() => {
engineCommandManager.addEventListener( engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady, EngineCommandManagerEvents.SceneReady,
async () => { () => {
sceneInfra.camControls.dollyZoom(fov) sceneInfra.camControls.dollyZoom(fov).catch(reportRejection)
} }
) )
}, []) }, [])
@ -26,11 +27,11 @@ export const CamToggle = () => {
if (isPerspective) { if (isPerspective) {
isReducedMotion() isReducedMotion()
? sceneInfra.camControls.useOrthographicCamera() ? sceneInfra.camControls.useOrthographicCamera()
: sceneInfra.camControls.animateToOrthographic() : sceneInfra.camControls.animateToOrthographic().catch(reportRejection)
} else { } else {
isReducedMotion() isReducedMotion()
? sceneInfra.camControls.usePerspectiveCamera() ? sceneInfra.camControls.usePerspectiveCamera().catch(reportRejection)
: sceneInfra.camControls.animateToPerspective() : sceneInfra.camControls.animateToPerspective().catch(reportRejection)
} }
setIsPerspective(!isPerspective) setIsPerspective(!isPerspective)
} }

View File

@ -1,53 +1,43 @@
import { useMachine } from '@xstate/react' import { createActorContext } from '@xstate/react'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { createContext, useEffect } from 'react' import { useEffect } from 'react'
import { EventFrom, StateFrom } from 'xstate'
type CommandsContextType = { export const CommandsContext = createActorContext(
commandBarState: StateFrom<typeof commandBarMachine> commandBarMachine.provide({
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
}
export const CommandsContext = createContext<CommandsContextType>({
commandBarState: commandBarMachine.initialState,
commandBarSend: () => {},
})
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
devTools: true,
guards: { guards: {
'Command has no arguments': (context, _event) => { 'Command has no arguments': ({ context }) => {
return ( return (
!context.selectedCommand?.args || !context.selectedCommand?.args ||
Object.keys(context.selectedCommand?.args).length === 0 Object.keys(context.selectedCommand?.args).length === 0
) )
}, },
'All arguments are skippable': (context, _event) => { 'All arguments are skippable': ({ context }) => {
return Object.values(context.selectedCommand!.args!).every( return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip (argConfig) => argConfig.skip
) )
}, },
}, },
}) })
)
useEffect(() => { export const CommandBarProvider = ({
editorManager.setCommandBarSend(commandBarSend) children,
}) }: {
children: React.ReactNode
}) => {
return ( return (
<CommandsContext.Provider <CommandsContext.Provider>
value={{ <CommandBarProviderInner>{children}</CommandBarProviderInner>
commandBarState,
commandBarSend,
}}
>
{children}
</CommandsContext.Provider> </CommandsContext.Provider>
) )
} }
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
const commandBarActor = CommandsContext.useActorRef()
useEffect(() => {
editorManager.setCommandBarSend(commandBarActor.send)
})
return children
}

View File

@ -52,7 +52,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
e.preventDefault() e.preventDefault()
commandBarSend({ commandBarSend({
type: 'Submit command', type: 'Submit command',
data: argumentsToSubmit, output: argumentsToSubmit,
}) })
} }

View File

@ -9,7 +9,7 @@ import {
getSelectionTypeDisplayText, getSelectionTypeDisplayText,
} from 'lib/selections' } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { StateFrom } from 'xstate' import { StateFrom } from 'xstate'
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = { const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
@ -48,15 +48,15 @@ function CommandBarSelectionInput({
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false) const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector) const selection = useSelector(arg.machineActor, selectionSelector)
const initSelectionsByType = useCallback(() => { const selectionsByType = useMemo(() => {
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1] const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
return !selectionRangeEnd || selectionRangeEnd === code.length return !selectionRangeEnd || selectionRangeEnd === code.length
? 'none' ? 'none'
: getSelectionType(selection) : getSelectionType(selection)
}, [selection, code]) }, [selection, code])
const selectionsByType = initSelectionsByType() const canSubmitSelection = useMemo<boolean>(
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>( () => canSubmitSelectionArg(selectionsByType, arg),
canSubmitSelectionArg(selectionsByType, arg) [selectionsByType]
) )
useEffect(() => { useEffect(() => {
@ -66,26 +66,18 @@ function CommandBarSelectionInput({
// Fast-forward through this arg if it's marked as skippable // Fast-forward through this arg if it's marked as skippable
// and we have a valid selection already // and we have a valid selection already
useEffect(() => { useEffect(() => {
console.log('selection input effect', {
selectionsByType,
canSubmitSelection,
arg,
})
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
const argValue = commandBarState.context.argumentsToSubmit[arg.name] const argValue = commandBarState.context.argumentsToSubmit[arg.name]
if (canSubmitSelection && arg.skip && argValue === undefined) { if (canSubmitSelection && arg.skip && argValue === undefined) {
handleSubmit({ handleSubmit()
preventDefault: () => {},
} as React.FormEvent<HTMLFormElement>)
} }
}, [selectionsByType, arg]) }, [canSubmitSelection])
function handleChange() { function handleChange() {
inputRef.current?.focus() inputRef.current?.focus()
} }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e.preventDefault() e?.preventDefault()
if (!canSubmitSelection) { if (!canSubmitSelection) {
setHasSubmitted(true) setHasSubmitted(true)

View File

@ -1,5 +1,6 @@
import { CommandLog } from 'lang/std/engineConnection' import { CommandLog } from 'lang/std/engineConnection'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
export function useEngineCommands(): [CommandLog[], () => void] { export function useEngineCommands(): [CommandLog[], () => void] {
@ -77,9 +78,11 @@ export const EngineCommands = () => {
/> />
<button <button
data-testid="custom-cmd-send-button" data-testid="custom-cmd-send-button"
onClick={() => onClick={() => {
engineCommandManager.sendSceneCommand(JSON.parse(customCmd)) engineCommandManager
} .sendSceneCommand(JSON.parse(customCmd))
.catch(reportRejection)
}}
> >
Send custom command Send custom command
</button> </button>

View File

@ -5,13 +5,12 @@ import { PATHS } from 'lib/paths'
import React, { createContext } from 'react' import React, { createContext } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {
Actor,
AnyStateMachine, AnyStateMachine,
ContextFrom, ContextFrom,
EventFrom,
InterpreterFrom,
Prop, Prop,
StateFrom, StateFrom,
assign, fromPromise,
} from 'xstate' } from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine' import { fileMachine } from 'machines/fileMachine'
@ -27,7 +26,7 @@ import { getNextDirName, getNextFileName } from 'lib/desktopFS'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
context: ContextFrom<T> context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'> send: Prop<Actor<T>, 'send'>
} }
export const FileContext = createContext( export const FileContext = createContext(
@ -43,239 +42,234 @@ export const FileMachineProvider = ({
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const [state, send] = useMachine(fileMachine, { const [state, send] = useMachine(
context: { fileMachine.provide({
project, actions: {
selectedDirectory: project, renameToastSuccess: ({ event }) => {
}, if (event.type !== 'xstate.done.actor.rename-file') return
actions: { toast.success(event.output.message)
navigateToFile: (context, event) => { },
if (event.data && 'name' in event.data) { createToastSuccess: ({ event }) => {
commandBarSend({ type: 'Close' }) if (event.type !== 'xstate.done.actor.create-and-open-file') return
navigate( toast.success(event.output.message)
`..${PATHS.FILE}/${encodeURIComponent( },
context.selectedDirectory + toastSuccess: ({ event }) => {
window.electron.path.sep + if (
event.data.name event.type !== 'xstate.done.actor.rename-file' &&
)}` event.type !== 'xstate.done.actor.delete-file'
) )
} else if ( return
event.data && toast.success(event.output.message)
'path' in event.data && },
event.data.path.endsWith(FILE_EXT) toastError: ({ event }) => {
) { if (event.type !== 'xstate.done.actor.rename-file') return
// Don't navigate to newly created directories toast.error(event.output.message)
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`) },
} navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file') return
if (event.output && 'name' in event.output) {
commandBarSend({ type: 'Close' })
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory +
window.electron.path.sep +
event.output.name
)}`
)
} else if (
event.output &&
'path' in event.output &&
event.output.path.endsWith(FILE_EXT)
) {
// Don't navigate to newly created directories
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.output.path)}`)
}
},
}, },
addFileToRenamingQueue: assign({ actors: {
itemsBeingRenamed: (context, event) => [ readFiles: fromPromise(async ({ input }) => {
...context.itemsBeingRenamed, const newFiles =
event.data.path, (isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
], []
}),
removeFileFromRenamingQueue: assign({
itemsBeingRenamed: (
context,
event: EventFrom<typeof fileMachine, 'done.invoke.rename-file'>
) =>
context.itemsBeingRenamed.filter(
(path) => path !== event.data.oldPath
),
}),
renameToastSuccess: (_, event) => toast.success(event.data.message),
createToastSuccess: (_, event) => toast.success(event.data.message),
toastSuccess: (_, event) =>
event.data && toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
},
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isDesktop()
? (await getProjectInfo(context.project.path)).children
: []
return {
...context.project,
children: newFiles,
}
},
createAndOpenFile: async (context, event) => {
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (event.data.makeDir) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, event.data.content ?? '')
}
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
}
},
createFile: async (context, event) => {
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (event.data.makeDir) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, event.data.content ?? '')
}
return {
path: createdPath,
}
},
renameFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Rename file'>
) => {
const { oldName, newName, isDir } = event.data
const name = newName
? newName.endsWith(FILE_EXT) || isDir
? newName
: newName + FILE_EXT
: DEFAULT_FILE_NAME
const oldPath = window.electron.path.join(
context.selectedDirectory.path,
oldName
)
const newPath = window.electron.path.join(
context.selectedDirectory.path,
name
)
// no-op
if (oldPath === newPath) {
return { return {
message: `Old is the same as new.`, ...input,
children: newFiles,
}
}),
createAndOpenFile: fromPromise(async ({ input }) => {
let createdName = input.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (input.makeDir) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, input.content ?? '')
}
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
}
}),
createFile: fromPromise(async ({ input }) => {
let createdName = input.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (input.makeDir) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, input.content ?? '')
}
return {
path: createdPath,
}
}),
renameFile: fromPromise(async ({ input }) => {
const { oldName, newName, isDir } = input
const name = newName
? newName.endsWith(FILE_EXT) || isDir
? newName
: newName + FILE_EXT
: DEFAULT_FILE_NAME
const oldPath = window.electron.path.join(
input.selectedDirectory.path,
oldName
)
const newPath = window.electron.path.join(
input.selectedDirectory.path,
name
)
// no-op
if (oldPath === newPath) {
return {
message: `Old is the same as new.`,
newPath,
oldPath,
}
}
// if there are any siblings with the same name, report error.
const entries = await window.electron.readdir(
window.electron.path.dirname(newPath)
)
for (let entry of entries) {
if (entry === newName) {
return Promise.reject(new Error('Filename already exists.'))
}
}
window.electron.rename(oldPath, newPath)
if (!file) {
return Promise.reject(new Error('file is not defined'))
}
if (oldPath === file.path && project?.path) {
// If we just renamed the current file, navigate to the new path
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
} else if (file?.path.includes(oldPath)) {
// If we just renamed a directory that the current file is in, navigate to the new path
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
file.path.replace(oldPath, newPath)
)}`
)
}
return {
message: `Successfully renamed "${oldName}" to "${name}"`,
newPath, newPath,
oldPath, oldPath,
} }
} }),
deleteFile: fromPromise(async ({ input }) => {
const isDir = !!input.children
// if there are any siblings with the same name, report error. if (isDir) {
const entries = await window.electron.readdir( await window.electron
window.electron.path.dirname(newPath) .rm(input.path, {
) recursive: true,
for (let entry of entries) { })
if (entry === newName) { .catch((e) => console.error('Error deleting directory', e))
return Promise.reject(new Error('Filename already exists.')) } else {
await window.electron
.rm(input.path)
.catch((e) => console.error('Error deleting file', e))
} }
}
window.electron.rename(oldPath, newPath) // If there are no more files at all in the project, create a main.kcl
// for when we navigate to the root.
if (!project?.path) {
return Promise.reject(new Error('Project path not set.'))
}
if (!file) { const entries = await window.electron.readdir(project.path)
return Promise.reject(new Error('file is not defined')) const hasKclEntries =
} entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
if (!hasKclEntries) {
await window.electron.writeFile(
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
''
)
// Refresh the route selected above because it's possible we're on
// the same path on the navigate, which doesn't cause anything to
// refresh, leaving a stale execution state.
navigate(0)
return {
message: 'No more files in project, created main.kcl',
}
}
if (oldPath === file.path && project?.path) { // If we just deleted the current file or one of its parent directories,
// If we just renamed the current file, navigate to the new path // navigate to the project root
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`) if (
} else if (file?.path.includes(oldPath)) { (input.path === file?.path || file?.path.includes(input.path)) &&
// If we just renamed a directory that the current file is in, navigate to the new path project?.path
navigate( ) {
`..${PATHS.FILE}/${encodeURIComponent( navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
file.path.replace(oldPath, newPath) }
)}`
)
}
return { return {
message: `Successfully renamed "${oldName}" to "${name}"`, message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
newPath, input.name
oldPath, }"`,
} }
}),
}, },
deleteFile: async ( }),
context: ContextFrom<typeof fileMachine>, {
event: EventFrom<typeof fileMachine, 'Delete file'> input: {
) => { project,
const isDir = !!event.data.children selectedDirectory: project,
if (isDir) {
await window.electron
.rm(event.data.path, {
recursive: true,
})
.catch((e) => console.error('Error deleting directory', e))
} else {
await window.electron
.rm(event.data.path)
.catch((e) => console.error('Error deleting file', e))
}
// If there are no more files at all in the project, create a main.kcl
// for when we navigate to the root.
if (!project?.path) {
return Promise.reject(new Error('Project path not set.'))
}
const entries = await window.electron.readdir(project.path)
const hasKclEntries =
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
if (!hasKclEntries) {
await window.electron.writeFile(
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
''
)
// Refresh the route selected above because it's possible we're on
// the same path on the navigate, which doesn't cause anything to
// refresh, leaving a stale execution state.
navigate(0)
return
}
// If we just deleted the current file or one of its parent directories,
// navigate to the project root
if (
(event.data.path === file?.path ||
file?.path.includes(event.data.path)) &&
project?.path
) {
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
}
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
event.data.name
}"`
}, },
}, }
guards: { )
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
if (event.type !== 'done.invoke.read-files') return false
return !!event?.data?.children && event.data.children.length > 0
},
'Is not silent': (_, event) => !event.data?.silent,
},
})
return ( return (
<FileContext.Provider <FileContext.Provider

View File

@ -176,9 +176,11 @@ const FileTreeItem = ({
`import("${fileOrDir.path.replace(project.path, '.')}")\n` + `import("${fileOrDir.path.replace(project.path, '.')}")\n` +
codeManager.code codeManager.code
) )
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.writeToFile() codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files // Prevent seeing the model built one piece at a time when changing files
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode(true) kclManager.executeCode(true)
} else { } else {
// Let the lsp servers know we closed a file. // Let the lsp servers know we closed a file.
@ -243,13 +245,13 @@ const FileTreeItem = ({
onClickCapture={(e) => onClickCapture={(e) =>
fileSend({ fileSend({
type: 'Set selected directory', type: 'Set selected directory',
data: fileOrDir, directory: fileOrDir,
}) })
} }
onFocusCapture={(e) => onFocusCapture={(e) =>
fileSend({ fileSend({
type: 'Set selected directory', type: 'Set selected directory',
data: fileOrDir, directory: fileOrDir,
}) })
} }
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()} onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
@ -296,13 +298,13 @@ const FileTreeItem = ({
onClickCapture={(e) => { onClickCapture={(e) => {
fileSend({ fileSend({
type: 'Set selected directory', type: 'Set selected directory',
data: fileOrDir, directory: fileOrDir,
}) })
}} }}
onFocusCapture={(e) => onFocusCapture={(e) =>
fileSend({ fileSend({
type: 'Set selected directory', type: 'Set selected directory',
data: fileOrDir, directory: fileOrDir,
}) })
} }
> >
@ -388,14 +390,14 @@ interface FileTreeProps {
export const FileTreeMenu = () => { export const FileTreeMenu = () => {
const { send } = useFileContext() const { send } = useFileContext()
async function createFile() { function createFile() {
send({ send({
type: 'Create file', type: 'Create file',
data: { name: '', makeDir: false }, data: { name: '', makeDir: false },
}) })
} }
async function createFolder() { function createFolder() {
send({ send({
type: 'Create file', type: 'Create file',
data: { name: '', makeDir: true }, data: { name: '', makeDir: true },
@ -482,7 +484,7 @@ export const FileTreeInner = ({
onClickCapture={(e) => { onClickCapture={(e) => {
fileSend({ fileSend({
type: 'Set selected directory', type: 'Set selected directory',
data: fileContext.project, directory: fileContext.project,
}) })
}} }}
> >

View File

@ -27,6 +27,7 @@ import {
} from './ContextMenu' } from './ContextMenu'
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { reportRejection } from 'lib/trap'
const CANVAS_SIZE = 80 const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5 const FRUSTUM_SIZE = 0.5
@ -67,7 +68,9 @@ export default function Gizmo() {
<ContextMenuItem <ContextMenuItem
key={axisName} key={axisName}
onClick={() => { onClick={() => {
sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames) sceneInfra.camControls
.updateCameraToAxis(axisName as AxisNames)
.catch(reportRejection)
}} }}
> >
{axisSemantic} view {axisSemantic} view
@ -75,7 +78,7 @@ export default function Gizmo() {
)), )),
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
sceneInfra.camControls.resetCameraPosition() sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}} }}
> >
Reset view Reset view
@ -299,7 +302,7 @@ const initializeMouseEvents = (
const handleClick = () => { const handleClick = () => {
if (raycasterIntersect.current) { if (raycasterIntersect.current) {
const axisName = raycasterIntersect.current.object.name as AxisNames const axisName = raycasterIntersect.current.object.name as AxisNames
sceneInfra.camControls.updateCameraToAxis(axisName) sceneInfra.camControls.updateCameraToAxis(axisName).catch(reportRejection)
} }
} }

View File

@ -8,6 +8,7 @@ import { createAndOpenNewProject } from 'lib/desktopFS'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
const HelpMenuDivider = () => ( const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" /> <div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -115,7 +116,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) { if (isInProject) {
navigate(filePath + PATHS.ONBOARDING.INDEX) navigate(filePath + PATHS.ONBOARDING.INDEX)
} else { } else {
createAndOpenNewProject({ onProjectOpen, navigate }) createAndOpenNewProject({ onProjectOpen, navigate }).catch(
reportRejection
)
} }
}} }}
> >

View File

@ -12,6 +12,7 @@ import { CoreDumpManager } from 'lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow' import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator' import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { ModelStateIndicator } from './ModelStateIndicator' import { ModelStateIndicator } from './ModelStateIndicator'
import { reportRejection } from 'lib/trap'
export function LowerRightControls({ export function LowerRightControls({
children, children,
@ -25,7 +26,7 @@ export function LowerRightControls({
const linkOverrideClassName = const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30' '!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
async function reportbug(event: { function reportbug(event: {
preventDefault: () => void preventDefault: () => void
stopPropagation: () => void stopPropagation: () => void
}) { }) {
@ -34,7 +35,9 @@ export function LowerRightControls({
if (!coreDumpManager) { if (!coreDumpManager) {
// open default reporting option // open default reporting option
openWindow('https://github.com/KittyCAD/modeling-app/issues/new/choose') openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
} else { } else {
toast toast
.promise( .promise(
@ -56,7 +59,7 @@ export function LowerRightControls({
if (err) { if (err) {
openWindow( openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose' 'https://github.com/KittyCAD/modeling-app/issues/new/choose'
) ).catch(reportRejection)
} }
}) })
} }

View File

@ -160,7 +160,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// Update the folding ranges, since the AST has changed. // Update the folding ranges, since the AST has changed.
// This is a hack since codemirror does not support async foldService. // This is a hack since codemirror does not support async foldService.
// When they do we can delete this. // When they do we can delete this.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
plugin.updateFoldingRanges() plugin.updateFoldingRanges()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
plugin.requestSemanticTokens() plugin.requestSemanticTokens()
break break
case 'kcl/memoryUpdated': case 'kcl/memoryUpdated':

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ import { editorShortcutMeta } from './KclEditorPane'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
export const KclEditorMenu = ({ children }: PropsWithChildren) => { export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } = const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
@ -47,7 +48,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
{convertToVarEnabled && ( {convertToVarEnabled && (
<Menu.Item> <Menu.Item>
<button <button
onClick={() => handleConvertToVarClick()} onClick={() => {
handleConvertToVarClick().catch(reportRejection)
}}
className={styles.button} className={styles.button}
> >
<span>Convert to Variable</span> <span>Convert to Variable</span>

View File

@ -57,6 +57,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
icon: 'printer3d', icon: 'printer3d',
iconClassName: '!p-0', iconClassName: '!p-0',
keybinding: 'Ctrl + Shift + M', keybinding: 'Ctrl + Shift + M',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => { action: async () => {
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',

View File

@ -4,6 +4,8 @@ import Tooltip from './Tooltip'
import { ConnectingTypeGroup } from '../lang/std/engineConnection' import { ConnectingTypeGroup } from '../lang/std/engineConnection'
import { useNetworkContext } from '../hooks/useNetworkContext' import { useNetworkContext } from '../hooks/useNetworkContext'
import { NetworkHealthState } from '../hooks/useNetworkStatus' import { NetworkHealthState } from '../hooks/useNetworkStatus'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = { export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
[NetworkHealthState.Ok]: 'Connected', [NetworkHealthState.Ok]: 'Connected',
@ -160,13 +162,13 @@ export const NetworkHealthIndicator = () => {
</div> </div>
{issues[name as ConnectingTypeGroup] && ( {issues[name as ConnectingTypeGroup] && (
<button <button
onClick={async () => { onClick={toSync(async () => {
await navigator.clipboard.writeText( await navigator.clipboard.writeText(
JSON.stringify(error, null, 2) || '' JSON.stringify(error, null, 2) || ''
) )
setHasCopied(true) setHasCopied(true)
setTimeout(() => setHasCopied(false), 5000) setTimeout(() => setHasCopied(false), 5000)
}} }, reportRejection)}
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80" className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
> >
{hasCopied ? 'Copied' : 'Copy Error'} {hasCopied ? 'Copied' : 'Copy Error'}

View File

@ -8,6 +8,8 @@ import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog' import { DeleteConfirmationDialog } from './DeleteProjectDialog'
import { ProjectCardRenameForm } from './ProjectCardRenameForm' import { ProjectCardRenameForm } from './ProjectCardRenameForm'
import { Project } from 'lib/project' import { Project } from 'lib/project'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
function ProjectCard({ function ProjectCard({
project, project,
@ -165,10 +167,10 @@ function ProjectCard({
{isConfirmingDelete && ( {isConfirmingDelete && (
<DeleteConfirmationDialog <DeleteConfirmationDialog
title="Delete Project" title="Delete Project"
onConfirm={async () => { onConfirm={toSync(async () => {
await handleDeleteProject(project) await handleDeleteProject(project)
setIsConfirmingDelete(false) setIsConfirmingDelete(false)
}} }, reportRejection)}
onDismiss={() => setIsConfirmingDelete(false)} onDismiss={() => setIsConfirmingDelete(false)}
> >
<p className="my-4"> <p className="my-4">

View File

@ -6,6 +6,8 @@ import React, { useMemo } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
export const RefreshButton = ({ children }: React.PropsWithChildren) => { export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
@ -50,11 +52,12 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
// Window may not be available in some environments // Window may not be available in some environments
window?.location.reload() window?.location.reload()
}) })
.catch(reportRejection)
} }
return ( return (
<button <button
onClick={refresh} onClick={toSync(refresh, reportRejection)}
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90" className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
> >
<CustomIcon name="exclamationMark" className="w-5 h-5" /> <CustomIcon name="exclamationMark" className="w-5 h-5" />

View File

@ -20,6 +20,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
import { useDotDotSlash } from 'hooks/useDotDotSlash' import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { ForwardedRef, forwardRef, useEffect } from 'react' import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
interface AllSettingsFieldsProps { interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel searchParamTab: SettingsLevel
@ -54,7 +56,7 @@ export const AllSettingsFields = forwardRef(
) )
: undefined : undefined
async function restartOnboarding() { function restartOnboarding() {
send({ send({
type: `set.app.onboardingStatus`, type: `set.app.onboardingStatus`,
data: { level: 'user', value: '' }, data: { level: 'user', value: '' },
@ -82,6 +84,7 @@ export const AllSettingsFields = forwardRef(
} }
} }
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
navigateToOnboardingStart() navigateToOnboardingStart()
}, [isFileSettings, navigate, state]) }, [isFileSettings, navigate, state])
@ -190,7 +193,7 @@ export const AllSettingsFields = forwardRef(
{isDesktop() && ( {isDesktop() && (
<ActionButton <ActionButton
Element="button" Element="button"
onClick={async () => { onClick={toSync(async () => {
const paths = await getSettingsFolderPaths( const paths = await getSettingsFolderPaths(
projectPath ? decodeURIComponent(projectPath) : undefined projectPath ? decodeURIComponent(projectPath) : undefined
) )
@ -199,7 +202,7 @@ export const AllSettingsFields = forwardRef(
return new Error('finalPath undefined') return new Error('finalPath undefined')
} }
window.electron.showInFolder(finalPath) window.electron.showInFolder(finalPath)
}} }, reportRejection)}
iconStart={{ iconStart={{
icon: 'folder', icon: 'folder',
size: 'sm', size: 'sm',
@ -211,14 +214,14 @@ export const AllSettingsFields = forwardRef(
)} )}
<ActionButton <ActionButton
Element="button" Element="button"
onClick={async () => { onClick={toSync(async () => {
const defaultDirectory = await getInitialDefaultDir() const defaultDirectory = await getInitialDefaultDir()
send({ send({
type: 'Reset settings', type: 'Reset settings',
defaultDirectory, defaultDirectory,
}) })
toast.success('Settings restored to default') toast.success('Settings restored to default')
}} }, reportRejection)}
iconStart={{ iconStart={{
icon: 'refresh', icon: 'refresh',
size: 'sm', size: 'sm',

View File

@ -8,7 +8,7 @@ import {
} from 'lib/settings/settingsTypes' } from 'lib/settings/settingsTypes'
import { getSettingInputType } from 'lib/settings/settingsUtils' import { getSettingInputType } from 'lib/settings/settingsUtils'
import { useMemo } from 'react' import { useMemo } from 'react'
import { Event } from 'xstate' import { EventFrom } from 'xstate'
interface SettingsFieldInputProps { interface SettingsFieldInputProps {
// We don't need the fancy types here, // We don't need the fancy types here,
@ -59,7 +59,7 @@ export function SettingsFieldInput({
level: settingsLevel, level: settingsLevel,
value: newValue, value: newValue,
}, },
} as unknown as Event<WildcardSetEvent>) } as unknown as EventFrom<WildcardSetEvent>)
}} }}
/> />
) )
@ -103,7 +103,7 @@ export function SettingsFieldInput({
level: settingsLevel, level: settingsLevel,
value: e.target.value, value: e.target.value,
}, },
} as unknown as Event<WildcardSetEvent>) } as unknown as EventFrom<WildcardSetEvent>)
} }
> >
{options && {options &&
@ -137,7 +137,7 @@ export function SettingsFieldInput({
level: settingsLevel, level: settingsLevel,
value: e.target.value, value: e.target.value,
}, },
} as unknown as Event<WildcardSetEvent>) } as unknown as EventFrom<WildcardSetEvent>)
} }
}} }}
/> />

View File

@ -14,13 +14,7 @@ import {
Themes, Themes,
} from 'lib/theme' } from 'lib/theme'
import decamelize from 'decamelize' import decamelize from 'decamelize'
import { import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
AnyStateMachine,
ContextFrom,
InterpreterFrom,
Prop,
StateFrom,
} from 'xstate'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons' import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
@ -39,7 +33,7 @@ import { saveSettings } from 'lib/settings/settingsUtils'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
context: ContextFrom<T> context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'> send: Prop<Actor<T>, 'send'>
} }
type SettingsAuthContextType = { type SettingsAuthContextType = {
@ -50,7 +44,7 @@ type SettingsAuthContextType = {
// a little hacky for sure, open to changing it // a little hacky for sure, open to changing it
// this implies that we should only even have one instance of this provider mounted at any one time // this implies that we should only even have one instance of this provider mounted at any one time
// but I think that's a safe assumption // but I think that's a safe assumption
let settingsStateRef: (typeof settingsMachine)['context'] | undefined let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
export const getSettingsState = () => settingsStateRef export const getSettingsState = () => settingsStateRef
export const SettingsAuthContext = createContext({} as SettingsAuthContextType) export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
@ -101,21 +95,20 @@ export const SettingsAuthProviderBase = ({
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const [settingsState, settingsSend, settingsActor] = useMachine( const [settingsState, settingsSend, settingsActor] = useMachine(
settingsMachine, settingsMachine.provide({
{
context: loadedSettings,
actions: { actions: {
//TODO: batch all these and if that's difficult to do from tsx, //TODO: batch all these and if that's difficult to do from tsx,
// make it easy to do // make it easy to do
setClientSideSceneUnits: (context, event) => { setClientSideSceneUnits: ({ context, event }) => {
const newBaseUnit = const newBaseUnit =
event.type === 'set.modeling.defaultUnit' event.type === 'set.modeling.defaultUnit'
? (event.data.value as BaseUnit) ? (event.data.value as BaseUnit)
: context.modeling.defaultUnit.current : context.modeling.defaultUnit.current
sceneInfra.baseUnit = newBaseUnit sceneInfra.baseUnit = newBaseUnit
}, },
setEngineTheme: (context) => { setEngineTheme: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(), cmd_id: uuidv4(),
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -126,6 +119,7 @@ export const SettingsAuthProviderBase = ({
}) })
const opposingTheme = getOppositeTheme(context.app.theme.current) const opposingTheme = getOppositeTheme(context.app.theme.current)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(), cmd_id: uuidv4(),
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -135,16 +129,17 @@ export const SettingsAuthProviderBase = ({
}, },
}) })
}, },
setEngineScaleGridVisibility: (context) => { setEngineScaleGridVisibility: ({ context }) => {
engineCommandManager.setScaleGridVisibility( engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current context.modeling.showScaleGrid.current
) )
}, },
setClientTheme: (context) => { setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current) const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme sceneInfra.theme = opposingTheme
}, },
setEngineEdges: (context) => { setEngineEdges: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(), cmd_id: uuidv4(),
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -154,7 +149,8 @@ export const SettingsAuthProviderBase = ({
}, },
}) })
}, },
toastSuccess: (_, event) => { toastSuccess: ({ event }) => {
if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [ const eventParts = event.type.replace(/^set./, '').split('.') as [
keyof typeof settings, keyof typeof settings,
string string
@ -176,7 +172,7 @@ export const SettingsAuthProviderBase = ({
id: `${event.type}.success`, id: `${event.type}.success`,
}) })
}, },
'Execute AST': (context, event) => { 'Execute AST': ({ context, event }) => {
try { try {
const allSettingsIncludesUnitChange = const allSettingsIncludesUnitChange =
event.type === 'Set all settings' && event.type === 'Set all settings' &&
@ -193,6 +189,7 @@ export const SettingsAuthProviderBase = ({
resetSettingsIncludesUnitChange resetSettingsIncludesUnitChange
) { ) {
// Unit changes requires a re-exec of code // Unit changes requires a re-exec of code
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode(true) kclManager.executeCode(true)
} else { } else {
// For any future logging we'd like to do // For any future logging we'd like to do
@ -204,12 +201,13 @@ export const SettingsAuthProviderBase = ({
console.error('Error executing AST after settings change', e) console.error('Error executing AST after settings change', e)
} }
}, },
persistSettings: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
saveSettings(context, loadedProject?.project?.path)
},
}, },
services: { }),
'Persist settings': (context) => { input: loadedSettings }
saveSettings(context, loadedProject?.project?.path),
},
}
) )
settingsStateRef = settingsState.context settingsStateRef = settingsState.context
@ -292,19 +290,22 @@ export const SettingsAuthProviderBase = ({
}, [settingsState.context.textEditor.blinkingCursor.current]) }, [settingsState.context.textEditor.blinkingCursor.current])
// Auth machine setup // Auth machine setup
const [authState, authSend, authActor] = useMachine(authMachine, { const [authState, authSend, authActor] = useMachine(
actions: { authMachine.provide({
goToSignInPage: () => { actions: {
navigate(PATHS.SIGN_IN) goToSignInPage: () => {
logout() navigate(PATHS.SIGN_IN)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
logout()
},
goToIndexPage: () => {
if (location.pathname.includes(PATHS.SIGN_IN)) {
navigate(PATHS.INDEX)
}
},
}, },
goToIndexPage: () => { })
if (location.pathname.includes(PATHS.SIGN_IN)) { )
navigate(PATHS.INDEX)
}
},
},
})
useStateMachineCommands({ useStateMachineCommands({
machineId: 'auth', machineId: 'auth',
@ -336,13 +337,11 @@ export const SettingsAuthProviderBase = ({
export default SettingsAuthProvider export default SettingsAuthProvider
export function logout() { export async function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY) localStorage.removeItem(TOKEN_PERSIST_KEY)
return ( if (isDesktop()) return Promise.resolve(null)
!isDesktop() && return fetch(withBaseUrl('/logout'), {
fetch(withBaseUrl('/logout'), { method: 'POST',
method: 'POST', credentials: 'include',
credentials: 'include', })
})
)
} }

View File

@ -53,9 +53,10 @@ export const Stream = () => {
* executed. If we can find a way to do this from a more * executed. If we can find a way to do this from a more
* central place, we can move this code there. * central place, we can move this code there.
*/ */
async function executeCodeAndPlayStream() { function executeCodeAndPlayStream() {
kclManager.executeCode(true).then(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises
videoRef.current?.play().catch((e) => { kclManager.executeCode(true).then(async () => {
await videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current) console.warn('Video playing was prevented', e, videoRef.current)
}) })
setStreamState(StreamState.Playing) setStreamState(StreamState.Playing)
@ -218,12 +219,12 @@ export const Stream = () => {
*/ */
useEffect(() => { useEffect(() => {
if (!kclManager.isExecuting) { if (!kclManager.isExecuting) {
setTimeout(() => setTimeout(() => {
// execute in the next event loop // execute in the next event loop
videoRef.current?.play().catch((e) => { videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current) console.warn('Video playing was prevented', e, videoRef.current)
}) })
) })
} }
}, [kclManager.isExecuting]) }, [kclManager.isExecuting])
@ -287,9 +288,10 @@ export const Stream = () => {
}, },
}) })
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('idle.showPlanes')) return if (state.matches({ idle: 'showPlanes' })) return
if (!context.store?.didDragInStream && btnName(e).left) { if (!context.store?.didDragInStream && btnName(e).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendSelectEventToEngine( sendSelectEventToEngine(
e, e,
videoRef.current, videoRef.current,

View File

@ -26,8 +26,9 @@ import { sendTelemetry } from 'lib/textToCad'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { EventData, EventFrom } from 'xstate' import { EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine' import { fileMachine } from 'machines/fileMachine'
import { reportRejection } from 'lib/trap'
const CANVAS_SIZE = 128 const CANVAS_SIZE = 128
const PROMPT_TRUNCATE_LENGTH = 128 const PROMPT_TRUNCATE_LENGTH = 128
@ -45,7 +46,7 @@ export function ToastTextToCadError({
prompt: string prompt: string
commandBarSend: ( commandBarSend: (
event: EventFrom<typeof commandBarMachine>, event: EventFrom<typeof commandBarMachine>,
data?: EventData data?: unknown
) => void ) => void
}) { }) {
return ( return (
@ -112,7 +113,7 @@ export function ToastTextToCadSuccess({
token?: string token?: string
fileMachineSend: ( fileMachineSend: (
event: EventFrom<typeof fileMachine>, event: EventFrom<typeof fileMachine>,
data?: EventData data?: unknown
) => void ) => void
settings: { settings: {
theme: Themes theme: Themes
@ -297,7 +298,7 @@ export function ToastTextToCadSuccess({
name={hasCopied ? 'Close' : 'Reject'} name={hasCopied ? 'Close' : 'Reject'}
onClick={() => { onClick={() => {
if (!hasCopied) { if (!hasCopied) {
sendTelemetry(modelId, 'rejected', token) sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
} }
if (isDesktop()) { if (isDesktop()) {
// Delete the file from the project // Delete the file from the project
@ -323,6 +324,7 @@ export function ToastTextToCadSuccess({
}} }}
name="Accept" name="Accept"
onClick={() => { onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendTelemetry(modelId, 'accepted', token) sendTelemetry(modelId, 'accepted', token)
navigate( navigate(
`${PATHS.FILE}/${encodeURIComponent( `${PATHS.FILE}/${encodeURIComponent(
@ -342,7 +344,9 @@ export function ToastTextToCadSuccess({
}} }}
name="Copy to clipboard" name="Copy to clipboard"
onClick={() => { onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendTelemetry(modelId, 'accepted', token) sendTelemetry(modelId, 'accepted', token)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
navigator.clipboard.writeText(data.code || '// no code found') navigator.clipboard.writeText(data.code || '// no code found')
setShowCopiedUi(true) setShowCopiedUi(true)
setHasCopied(true) setHasCopied(true)

View File

@ -133,7 +133,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
Element: 'button', Element: 'button',
'data-testid': 'user-sidebar-sign-out', 'data-testid': 'user-sidebar-sign-out',
children: 'Sign out', children: 'Sign out',
onClick: () => send('Log out'), onClick: () => send({ type: 'Log out' }),
className: '', // Just making TS's filter type coercion happy 😠 className: '', // Just making TS's filter type coercion happy 😠
}, },
].filter( ].filter(

View File

@ -1,7 +1,7 @@
import { EditorView, ViewUpdate } from '@codemirror/view' import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, Annotation, Transaction } from '@codemirror/state' import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections' import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands' import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { CommandBarMachineEvent } from 'machines/commandBarMachine'
@ -11,6 +11,7 @@ import {
forEachDiagnostic, forEachDiagnostic,
setDiagnosticsEffect, setDiagnosticsEffect,
} from '@codemirror/lint' } from '@codemirror/lint'
import { StateFrom } from 'xstate'
const updateOutsideEditorAnnotation = Annotation.define<boolean>() const updateOutsideEditorAnnotation = Annotation.define<boolean>()
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true) export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
@ -38,7 +39,7 @@ export default class EditorManager {
private _lastEvent: { event: string; time: number } | null = null private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {} private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
private _modelingEvent: ModelingMachineEvent | null = null private _modelingState: StateFrom<typeof modelingMachine> | null = null
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void = private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
() => {} () => {}
@ -80,8 +81,8 @@ export default class EditorManager {
this._modelingSend = send this._modelingSend = send
} }
set modelingEvent(event: ModelingMachineEvent) { set modelingState(state: StateFrom<typeof modelingMachine>) {
this._modelingEvent = event this._modelingState = state
} }
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) { setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
@ -248,13 +249,11 @@ export default class EditorManager {
return return
} }
const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool'] if (!this._modelingState) {
if (!this._modelingEvent) {
return return
} }
if (ignoreEvents.includes(this._modelingEvent.type)) { if (this._modelingState.matches({ Sketch: 'Change Tool' })) {
return return
} }
@ -286,8 +285,9 @@ export default class EditorManager {
this._lastEvent = { event: stringEvent, time: Date.now() } this._lastEvent = { event: stringEvent, time: Date.now() }
this._modelingSend(eventInfo.modelingEvent) this._modelingSend(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) => eventInfo.engineEvents.forEach((event) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand(event) engineCommandManager.sendSceneCommand(event)
) })
} }
} }

View File

@ -36,6 +36,7 @@ import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompleti
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams' import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams' import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
const copilotPluginAnnotation = Annotation.define<boolean>() const copilotPluginAnnotation = Annotation.define<boolean>()
export const copilotPluginEvent = copilotPluginAnnotation.of(true) export const copilotPluginEvent = copilotPluginAnnotation.of(true)
@ -266,7 +267,7 @@ export class CompletionRequester implements PluginValue {
if (!this.client.ready) return if (!this.client.ready) return
try { try {
this.requestCompletions() this.requestCompletions().catch(reportRejection)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@ -462,7 +463,7 @@ export class CompletionRequester implements PluginValue {
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)], annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
}) })
this.accept(ghostText.uuid) this.accept(ghostText.uuid).catch(reportRejection)
return true return true
} }
@ -490,7 +491,7 @@ export class CompletionRequester implements PluginValue {
], ],
}) })
this.reject() this.reject().catch(reportRejection)
return false return false
} }

View File

@ -96,6 +96,7 @@ export class KclPlugin implements PluginValue {
const newCode = viewUpdate.state.doc.toString() const newCode = viewUpdate.state.doc.toString()
codeManager.code = newCode codeManager.code = newCode
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.writeToFile() codeManager.writeToFile()
this.scheduleUpdateDoc() this.scheduleUpdateDoc()
@ -117,6 +118,7 @@ export class KclPlugin implements PluginValue {
} }
if (!this.client.ready) return if (!this.client.ready) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode() kclManager.executeCode()
} }

View File

@ -18,7 +18,7 @@ import {
CopilotWorkerOptions, CopilotWorkerOptions,
} from 'editor/plugins/lsp/types' } from 'editor/plugins/lsp/types'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
import { err } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
const intoServer: IntoServer = new IntoServer() const intoServer: IntoServer = new IntoServer()
const fromServer: FromServer | Error = FromServer.create() const fromServer: FromServer | Error = FromServer.create()
@ -60,7 +60,8 @@ export async function kclLspRun(
} }
} }
onmessage = function (event) { // WebWorker message handler.
onmessage = function (event: MessageEvent) {
if (err(fromServer)) return if (err(fromServer)) return
const { worker, eventType, eventData }: LspWorkerEvent = event.data const { worker, eventType, eventData }: LspWorkerEvent = event.data
@ -70,7 +71,7 @@ onmessage = function (event) {
| KclWorkerOptions | KclWorkerOptions
| CopilotWorkerOptions | CopilotWorkerOptions
initialise(wasmUrl) initialise(wasmUrl)
.then((instantiatedModule) => { .then(async (instantiatedModule) => {
console.log('Worker: WASM module loaded', worker, instantiatedModule) console.log('Worker: WASM module loaded', worker, instantiatedModule)
const config = new ServerConfig( const config = new ServerConfig(
intoServer, intoServer,
@ -81,7 +82,7 @@ onmessage = function (event) {
switch (worker) { switch (worker) {
case LspWorker.Kcl: case LspWorker.Kcl:
const kclData = eventData as KclWorkerOptions const kclData = eventData as KclWorkerOptions
kclLspRun( await kclLspRun(
config, config,
null, null,
kclData.token, kclData.token,
@ -91,7 +92,11 @@ onmessage = function (event) {
break break
case LspWorker.Copilot: case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions let copilotData = eventData as CopilotWorkerOptions
copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl) await copilotLspRun(
config,
copilotData.token,
copilotData.apiBaseUrl
)
break break
} }
}) })
@ -104,7 +109,7 @@ onmessage = function (event) {
intoServer.enqueue(data) intoServer.enqueue(data)
const json: jsrpc.JSONRPCRequest = Codec.decode(data) const json: jsrpc.JSONRPCRequest = Codec.decode(data)
if (null != json.id) { if (null != json.id) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion
fromServer.responses.get(json.id)!.then((response) => { fromServer.responses.get(json.id)!.then((response) => {
const encoded = Codec.encode(response as jsrpc.JSONRPCResponse) const encoded = Codec.encode(response as jsrpc.JSONRPCResponse)
postMessage(encoded) postMessage(encoded)
@ -115,19 +120,17 @@ onmessage = function (event) {
console.error('Worker: Unknown message type', worker, eventType) console.error('Worker: Unknown message type', worker, eventType)
} }
} }
;(async () => {
new Promise<void>(async (resolve) => {
if (err(fromServer)) return if (err(fromServer)) return
for await (const requests of fromServer.requests) { for await (const requests of fromServer.requests) {
const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest) const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest)
postMessage(encoded) postMessage(encoded)
} }
}) })().catch(reportRejection)
;(async () => {
new Promise<void>(async (resolve) => {
if (err(fromServer)) return if (err(fromServer)) return
for await (const notification of fromServer.notifications) { for await (const notification of fromServer.notifications) {
const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest) const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest)
postMessage(encoded) postMessage(encoded)
} }
}) })().catch(reportRejection)

View File

@ -1,6 +1,10 @@
import { CommandsContext } from 'components/CommandBar/CommandBarProvider' import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
import { useContext } from 'react'
export const useCommandsContext = () => { export const useCommandsContext = () => {
return useContext(CommandsContext) const commandBarActor = CommandsContext.useActorRef()
const commandBarState = CommandsContext.useSelector((state) => state)
return {
commandBarSend: commandBarActor.send,
commandBarState,
}
} }

View File

@ -14,7 +14,7 @@ import {
getSolid2dCodeRef, getSolid2dCodeRef,
getWallCodeRef, getWallCodeRef,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { err } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities' import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
@ -86,9 +86,11 @@ export function useEngineConnectionSubscriptions() {
}) })
const unSubClick = engineCommandManager.subscribeTo({ const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point', event: 'select_with_point',
callback: async (engineEvent) => { callback: (engineEvent) => {
const event = await getEventForSelectWithPoint(engineEvent) ;(async () => {
event && send(event) const event = await getEventForSelectWithPoint(engineEvent)
event && send(event)
})().catch(reportRejection)
}, },
}) })
return () => { return () => {
@ -101,118 +103,120 @@ export function useEngineConnectionSubscriptions() {
const unSub = engineCommandManager.subscribeTo({ const unSub = engineCommandManager.subscribeTo({
event: 'select_with_point', event: 'select_with_point',
callback: state.matches('Sketch no face') callback: state.matches('Sketch no face')
? async ({ data }) => { ? ({ data }) => {
let planeOrFaceId = data.entity_id ;(async () => {
if (!planeOrFaceId) return let planeOrFaceId = data.entity_id
if ( if (!planeOrFaceId) return
engineCommandManager.defaultPlanes?.xy === planeOrFaceId || if (
engineCommandManager.defaultPlanes?.xz === planeOrFaceId || engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
engineCommandManager.defaultPlanes?.yz === planeOrFaceId || engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
engineCommandManager.defaultPlanes?.negXy === planeOrFaceId || engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
engineCommandManager.defaultPlanes?.negXz === planeOrFaceId || engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
engineCommandManager.defaultPlanes?.negYz === planeOrFaceId engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
) { engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
let planeId = planeOrFaceId ) {
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = { let planeId = planeOrFaceId
[engineCommandManager.defaultPlanes.xy]: 'XY', const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
[engineCommandManager.defaultPlanes.xz]: 'XZ', [engineCommandManager.defaultPlanes.xy]: 'XY',
[engineCommandManager.defaultPlanes.yz]: 'YZ', [engineCommandManager.defaultPlanes.xz]: 'XZ',
[engineCommandManager.defaultPlanes.negXy]: '-XY', [engineCommandManager.defaultPlanes.yz]: 'YZ',
[engineCommandManager.defaultPlanes.negXz]: '-XZ', [engineCommandManager.defaultPlanes.negXy]: '-XY',
[engineCommandManager.defaultPlanes.negYz]: '-YZ', [engineCommandManager.defaultPlanes.negXz]: '-XZ',
} [engineCommandManager.defaultPlanes.negYz]: '-YZ',
// TODO can we get this information from rust land when it creates the default planes? }
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs) // TODO can we get this information from rust land when it creates the default planes?
let zAxis: [number, number, number] = [0, 0, 1] // maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
let yAxis: [number, number, number] = [0, 1, 0] let zAxis: [number, number, number] = [0, 0, 1]
let yAxis: [number, number, number] = [0, 1, 0]
// get unit vector from camera position to target // get unit vector from camera position to target
const camVector = sceneInfra.camControls.camera.position const camVector = sceneInfra.camControls.camera.position
.clone() .clone()
.sub(sceneInfra.camControls.target) .sub(sceneInfra.camControls.target)
if (engineCommandManager.defaultPlanes?.xy === planeId) { if (engineCommandManager.defaultPlanes?.xy === planeId) {
zAxis = [0, 0, 1] zAxis = [0, 0, 1]
yAxis = [0, 1, 0] yAxis = [0, 1, 0]
if (camVector.z < 0) { if (camVector.z < 0) {
zAxis = [0, 0, -1] zAxis = [0, 0, -1]
planeId = engineCommandManager.defaultPlanes?.negXy || '' planeId = engineCommandManager.defaultPlanes?.negXy || ''
} }
} else if (engineCommandManager.defaultPlanes?.yz === planeId) { } else if (engineCommandManager.defaultPlanes?.yz === planeId) {
zAxis = [1, 0, 0] zAxis = [1, 0, 0]
yAxis = [0, 0, 1] yAxis = [0, 0, 1]
if (camVector.x < 0) { if (camVector.x < 0) {
zAxis = [-1, 0, 0] zAxis = [-1, 0, 0]
planeId = engineCommandManager.defaultPlanes?.negYz || '' planeId = engineCommandManager.defaultPlanes?.negYz || ''
} }
} else if (engineCommandManager.defaultPlanes?.xz === planeId) { } else if (engineCommandManager.defaultPlanes?.xz === planeId) {
zAxis = [0, 1, 0] zAxis = [0, 1, 0]
yAxis = [0, 0, 1] yAxis = [0, 0, 1]
planeId = engineCommandManager.defaultPlanes?.negXz || '' planeId = engineCommandManager.defaultPlanes?.negXz || ''
if (camVector.y < 0) { if (camVector.y < 0) {
zAxis = [0, -1, 0] zAxis = [0, -1, 0]
planeId = engineCommandManager.defaultPlanes?.xz || '' planeId = engineCommandManager.defaultPlanes?.xz || ''
}
} }
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'defaultPlane',
planeId: planeId,
plane: defaultPlaneStrMap[planeId],
zAxis,
yAxis,
},
})
return
} }
const faceId = planeOrFaceId
const artifact = engineCommandManager.artifactGraph.get(faceId)
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
faceId,
engineCommandManager.artifactGraph
)
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
const codeRef =
artifact.type === 'cap'
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
const faceInfo = await getFaceDetails(faceId)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
err(codeRef) ? [0, 0] : codeRef.range
)
const extrudePathToNode = !err(extrusion)
? getNodePathFromSourceRange(
kclManager.ast,
extrusion.codeRef.range
)
: []
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Select default plane', type: 'Select default plane',
data: { data: {
type: 'defaultPlane', type: 'extrudeFace',
planeId: planeId, zAxis: [z_axis.x, z_axis.y, z_axis.z],
plane: defaultPlaneStrMap[planeId], yAxis: [y_axis.x, y_axis.y, y_axis.z],
zAxis, position: [origin.x, origin.y, origin.z].map(
yAxis, (num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap: artifact.type === 'cap' ? artifact.subType : 'none',
faceId: faceId,
}, },
}) })
return return
} })().catch(reportRejection)
const faceId = planeOrFaceId
const artifact = engineCommandManager.artifactGraph.get(faceId)
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
faceId,
engineCommandManager.artifactGraph
)
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
const codeRef =
artifact.type === 'cap'
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
const faceInfo = await getFaceDetails(faceId)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
err(codeRef) ? [0, 0] : codeRef.range
)
const extrudePathToNode = !err(extrusion)
? getNodePathFromSourceRange(
kclManager.ast,
extrusion.codeRef.range
)
: []
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap: artifact.type === 'cap' ? artifact.subType : 'none',
faceId: faceId,
},
})
return
} }
: () => {}, : () => {},
}) })

View File

@ -23,7 +23,8 @@ export function useRefreshSettings(routeId: string = PATHS.INDEX) {
} }
useEffect(() => { useEffect(() => {
ctx.settings.send('Set all settings', { ctx.settings.send({
type: 'Set all settings',
settings: routeData, settings: routeData,
}) })
}, []) }, [])

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate' import { AnyStateMachine, Actor, StateFrom } from 'xstate'
import { createMachineCommand } from '../lib/createMachineCommand' import { createMachineCommand } from '../lib/createMachineCommand'
import { useCommandsContext } from './useCommandsContext' import { useCommandsContext } from './useCommandsContext'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine } from 'machines/modelingMachine'
@ -15,6 +15,7 @@ import { useKclContext } from 'lang/KclProvider'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { useAppState } from 'AppState' import { useAppState } from 'AppState'
import { getActorNextEvents } from 'lib/utils'
// This might not be necessary, AnyStateMachine from xstate is working // This might not be necessary, AnyStateMachine from xstate is working
export type AllMachines = export type AllMachines =
@ -30,7 +31,7 @@ interface UseStateMachineCommandsArgs<
machineId: T['id'] machineId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: Actor<T>
commandBarConfig?: StateMachineCommandSetConfig<T, S> commandBarConfig?: StateMachineCommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean allCommandsRequireNetwork?: boolean
onCancel?: () => void onCancel?: () => void
@ -59,7 +60,7 @@ export default function useStateMachineCommands<
overallState !== NetworkHealthState.Weak) || overallState !== NetworkHealthState.Weak) ||
isExecuting || isExecuting ||
!isStreamReady !isStreamReady
const newCommands = state.nextEvents const newCommands = getActorNextEvents(state)
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons) .filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.flatMap((type) => .flatMap((type) =>

View File

@ -3,13 +3,14 @@ import {
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { editorManager, kclManager } from 'lib/singletons' import { editorManager, kclManager } from 'lib/singletons'
import { trap } from 'lib/trap' import { reportRejection, trap } from 'lib/trap'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange } from 'lang/wasm' import { PathToNode, SourceRange } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { toSync } from 'lib/utils'
export const getVarNameModal = createSetVarNameModal(SetVarNameModal) export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
@ -62,7 +63,7 @@ export function useConvertToVariable(range?: SourceRange) {
} }
} }
editorManager.convertToVariableCallback = handleClick editorManager.convertToVariableCallback = toSync(handleClick, reportRejection)
return { enable, handleClick } return { enable, handleClick }
} }

View File

@ -50,14 +50,6 @@ body.dark {
@apply text-chalkboard-10; @apply text-chalkboard-10;
} }
@media (prefers-color-scheme: dark) {
body,
.body-bg,
.dark .body-bg {
@apply bg-chalkboard-100;
}
}
select { select {
@apply bg-chalkboard-20; @apply bg-chalkboard-20;
} }

View File

@ -129,8 +129,8 @@ export class KclManager {
if (!isExecuting && this.executeIsStale) { if (!isExecuting && this.executeIsStale) {
const args = this.executeIsStale const args = this.executeIsStale
this.executeIsStale = null this.executeIsStale = null
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.executeAst(args) this.executeAst(args)
} else {
} }
this._isExecutingCallback(isExecuting) this._isExecutingCallback(isExecuting)
} }
@ -154,6 +154,7 @@ export class KclManager {
constructor(engineCommandManager: EngineCommandManager) { constructor(engineCommandManager: EngineCommandManager) {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ensureWasmInit().then(() => { this.ensureWasmInit().then(() => {
this.ast = this.safeParse(codeManager.code) || this.ast this.ast = this.safeParse(codeManager.code) || this.ast
}) })
@ -400,9 +401,11 @@ export class KclManager {
// Update the code state and the editor. // Update the code state and the editor.
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
// Write back to the file system. // Write back to the file system.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.writeToFile() codeManager.writeToFile()
// execute the code. // execute the code.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.executeCode() this.executeCode()
} }
// There's overlapping responsibility between updateAst and executeAst. // There's overlapping responsibility between updateAst and executeAst.
@ -541,6 +544,7 @@ function defaultSelectionFilter(
programMemory: ProgramMemory, programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
programMemory.hasSketchOrExtrudeGroup() && programMemory.hasSketchOrExtrudeGroup() &&
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',

View File

@ -62,6 +62,7 @@ export async function executeAst({
try { try {
if (!useFakeExecutor) { if (!useFakeExecutor) {
engineCommandManager.endSession() engineCommandManager.endSession()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
} }
const programMemory = await (useFakeExecutor const programMemory = await (useFakeExecutor

View File

@ -1,5 +1,5 @@
import { Selection } from 'lib/selections' import { Selection } from 'lib/selections'
import { err, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { import {
Program, Program,
CallExpression, CallExpression,
@ -904,115 +904,119 @@ export async function deleteFromSelection(
const expressionIndex = pathToNode[1][0] as number const expressionIndex = pathToNode[1][0] as number
astClone.body.splice(expressionIndex, 1) astClone.body.splice(expressionIndex, 1)
if (extrudeNameToDelete) { if (extrudeNameToDelete) {
await new Promise(async (resolve) => { await new Promise((resolve) => {
let currentVariableName = '' ;(async () => {
const pathsDependingOnExtrude: Array<{ let currentVariableName = ''
path: PathToNode const pathsDependingOnExtrude: Array<{
sketchName: string path: PathToNode
}> = [] sketchName: string
traverse(astClone, { }> = []
leave: (node) => { traverse(astClone, {
if (node.type === 'VariableDeclaration') { leave: (node) => {
currentVariableName = '' if (node.type === 'VariableDeclaration') {
} currentVariableName = ''
}, }
enter: async (node, path) => { },
if (node.type === 'VariableDeclaration') { enter: (node, path) => {
currentVariableName = node.declarations[0].id.name ;(async () => {
} if (node.type === 'VariableDeclaration') {
if ( currentVariableName = node.declarations[0].id.name
// match startSketchOn(${extrudeNameToDelete}) }
node.type === 'CallExpression' && if (
node.callee.name === 'startSketchOn' && // match startSketchOn(${extrudeNameToDelete})
node.arguments[0].type === 'Identifier' && node.type === 'CallExpression' &&
node.arguments[0].name === extrudeNameToDelete node.callee.name === 'startSketchOn' &&
) { node.arguments[0].type === 'Identifier' &&
pathsDependingOnExtrude.push({ node.arguments[0].name === extrudeNameToDelete
path, ) {
sketchName: currentVariableName, pathsDependingOnExtrude.push({
}) path,
} sketchName: currentVariableName,
}, })
}) }
const roundLiteral = (x: number) => createLiteral(roundOff(x)) })().catch(reportRejection)
const modificationDetails: { },
parent: PipeExpression['body']
faceDetails: Models['FaceIsPlanar_type']
lastKey: number
}[] = []
for (const { path, sketchName } of pathsDependingOnExtrude) {
const parent = getNodeFromPath<PipeExpression['body']>(
astClone,
path.slice(0, -1)
)
if (err(parent)) {
return
}
const sketchToPreserve = sketchGroupFromKclValue(
programMemory.get(sketchName),
sketchName
)
if (err(sketchToPreserve)) return sketchToPreserve
console.log('sketchName', sketchName)
// Can't kick off multiple requests at once as getFaceDetails
// is three engine calls in one and they conflict
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
if (
!(
faceDetails.origin &&
faceDetails.x_axis &&
faceDetails.y_axis &&
faceDetails.z_axis
)
) {
return
}
const lastKey = Number(path.slice(-1)[0][0])
modificationDetails.push({
parent: parent.node,
faceDetails,
lastKey,
}) })
} const roundLiteral = (x: number) => createLiteral(roundOff(x))
for (const { parent, faceDetails, lastKey } of modificationDetails) { const modificationDetails: {
if ( parent: PipeExpression['body']
!( faceDetails: Models['FaceIsPlanar_type']
faceDetails.origin && lastKey: number
faceDetails.x_axis && }[] = []
faceDetails.y_axis && for (const { path, sketchName } of pathsDependingOnExtrude) {
faceDetails.z_axis const parent = getNodeFromPath<PipeExpression['body']>(
astClone,
path.slice(0, -1)
) )
) { if (err(parent)) {
continue return
}
const sketchToPreserve = sketchGroupFromKclValue(
programMemory.get(sketchName),
sketchName
)
if (err(sketchToPreserve)) return sketchToPreserve
console.log('sketchName', sketchName)
// Can't kick off multiple requests at once as getFaceDetails
// is three engine calls in one and they conflict
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
if (
!(
faceDetails.origin &&
faceDetails.x_axis &&
faceDetails.y_axis &&
faceDetails.z_axis
)
) {
return
}
const lastKey = Number(path.slice(-1)[0][0])
modificationDetails.push({
parent: parent.node,
faceDetails,
lastKey,
})
} }
parent[lastKey] = createCallExpressionStdLib('startSketchOn', [ for (const { parent, faceDetails, lastKey } of modificationDetails) {
createObjectExpression({ if (
plane: createObjectExpression({ !(
origin: createObjectExpression({ faceDetails.origin &&
x: roundLiteral(faceDetails.origin.x), faceDetails.x_axis &&
y: roundLiteral(faceDetails.origin.y), faceDetails.y_axis &&
z: roundLiteral(faceDetails.origin.z), faceDetails.z_axis
}), )
x_axis: createObjectExpression({ ) {
x: roundLiteral(faceDetails.x_axis.x), continue
y: roundLiteral(faceDetails.x_axis.y), }
z: roundLiteral(faceDetails.x_axis.z), parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
}), createObjectExpression({
y_axis: createObjectExpression({ plane: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x), origin: createObjectExpression({
y: roundLiteral(faceDetails.y_axis.y), x: roundLiteral(faceDetails.origin.x),
z: roundLiteral(faceDetails.y_axis.z), y: roundLiteral(faceDetails.origin.y),
}), z: roundLiteral(faceDetails.origin.z),
z_axis: createObjectExpression({ }),
x: roundLiteral(faceDetails.z_axis.x), x_axis: createObjectExpression({
y: roundLiteral(faceDetails.z_axis.y), x: roundLiteral(faceDetails.x_axis.x),
z: roundLiteral(faceDetails.z_axis.z), y: roundLiteral(faceDetails.x_axis.y),
z: roundLiteral(faceDetails.x_axis.z),
}),
y_axis: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x),
y: roundLiteral(faceDetails.y_axis.y),
z: roundLiteral(faceDetails.y_axis.z),
}),
z_axis: createObjectExpression({
x: roundLiteral(faceDetails.z_axis.x),
y: roundLiteral(faceDetails.z_axis.y),
z: roundLiteral(faceDetails.z_axis.z),
}),
}), }),
}), }),
}), ])
]) }
} resolve(true)
resolve(true) })().catch(reportRejection)
}) })
} }
// await prom // await prom

View File

@ -36,7 +36,7 @@ beforeAll(async () => {
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {}, modifyGrid: async () => {},
callbackOnEngineLiteConnect: async () => { callbackOnEngineLiteConnect: () => {
resolve(true) resolve(true)
}, },
}) })

View File

@ -56,6 +56,7 @@ export function applyFilletToSelection(
const { modifiedAst, pathToFilletNode } = result const { modifiedAst, pathToFilletNode } = result
// 3. update ast // 3. update ast
// eslint-disable-next-line @typescript-eslint/no-floating-promises
updateAstAndFocus(modifiedAst, pathToFilletNode) updateAstAndFocus(modifiedAst, pathToFilletNode)
} }

View File

@ -124,6 +124,7 @@ beforeAll(async () => {
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {}, modifyGrid: async () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => { callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [ const cacheEntries = Object.entries(codeToWriteCacheFor) as [
CodeKey, CodeKey,

View File

@ -18,6 +18,7 @@ import toast from 'react-hot-toast'
import { SettingsViaQueryString } from 'lib/settings/settingsTypes' import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants' import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { KclManager } from 'lang/KclSingleton' import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
// TODO(paultag): This ought to be tweakable. // TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000 const pingIntervalMs = 5_000
@ -388,11 +389,12 @@ class EngineConnection extends EventTarget {
default: default:
if (this.isConnecting()) break if (this.isConnecting()) break
// Means we never could do an initial connection. Reconnect everything. // Means we never could do an initial connection. Reconnect everything.
if (!this.pingPongSpan.ping) this.connect() if (!this.pingPongSpan.ping) this.connect().catch(reportRejection)
break break
} }
}, pingIntervalMs) }, pingIntervalMs)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.connect() this.connect()
} }
@ -1464,6 +1466,7 @@ export class EngineCommandManager extends EventTarget {
}) })
) )
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.onEngineConnectionOpened = async () => { this.onEngineConnectionOpened = async () => {
// Set the stream background color // Set the stream background color
// This takes RGBA values from 0-1 // This takes RGBA values from 0-1
@ -1480,6 +1483,7 @@ export class EngineCommandManager extends EventTarget {
// Sets the default line colors // Sets the default line colors
const opposingTheme = getOppositeTheme(this.settings.theme) const opposingTheme = getOppositeTheme(this.settings.theme)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendSceneCommand({ this.sendSceneCommand({
cmd_id: uuidv4(), cmd_id: uuidv4(),
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -1490,6 +1494,7 @@ export class EngineCommandManager extends EventTarget {
}) })
// Set the edge lines visibility // Set the edge lines visibility
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendSceneCommand({ this.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -1500,6 +1505,7 @@ export class EngineCommandManager extends EventTarget {
}) })
this._camControlsCameraChange() this._camControlsCameraChange()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendSceneCommand({ this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events // CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially // firing this at connection ensure the camera's are synced initially
@ -1512,6 +1518,7 @@ export class EngineCommandManager extends EventTarget {
// We want modify the grid first because we don't want it to flash. // We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do // Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282 // that) https://github.com/KittyCAD/engine/issues/2282
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => { this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
await this.initPlanes() await this.initPlanes()
setIsStreamReady(true) setIsStreamReady(true)
@ -1715,6 +1722,7 @@ export class EngineCommandManager extends EventTarget {
this.onEngineConnectionNewTrack as EventListener this.onEngineConnectionNewTrack as EventListener
) )
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineConnection?.connect() this.engineConnection?.connect()
} }
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
@ -2125,6 +2133,7 @@ export class EngineCommandManager extends EventTarget {
* @param visible - whether to show or hide the scale grid * @param visible - whether to show or hide the scale grid
*/ */
setScaleGridVisibility(visible: boolean) { setScaleGridVisibility(visible: boolean) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!visible) this.modifyGrid(!visible)
} }

View File

@ -360,6 +360,7 @@ export const executor = async (
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
if (err(programMemory)) return Promise.reject(programMemory) if (err(programMemory)) return Promise.reject(programMemory)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const _programMemory = await _executor( const _programMemory = await _executor(
node, node,
@ -569,6 +570,7 @@ export async function coreDump(
a new GitHub issue for the user. a new GitHub issue for the user.
*/ */
if (openGithubIssue && dump.github_issue_url) { if (openGithubIssue && dump.github_issue_url) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
openWindow(dump.github_issue_url) openWindow(dump.github_issue_url)
} else { } else {
console.error( console.error(

View File

@ -10,7 +10,7 @@ import {
} from 'lib/settings/settingsTypes' } from 'lib/settings/settingsTypes'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { PathValue } from 'lib/types' import { PathValue } from 'lib/types'
import { AnyStateMachine, ContextFrom, InterpreterFrom } from 'xstate' import { Actor, AnyStateMachine, ContextFrom } from 'xstate'
import { getPropertyByPath } from 'lib/objectPropertyByPath' import { getPropertyByPath } from 'lib/objectPropertyByPath'
import { buildCommandArgument } from 'lib/createMachineCommand' import { buildCommandArgument } from 'lib/createMachineCommand'
import decamelize from 'decamelize' import decamelize from 'decamelize'
@ -28,7 +28,7 @@ export const settingsWithCommandConfigs = (
) as SettingsPaths[] ) as SettingsPaths[]
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>( const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
actor: InterpreterFrom<T>, actor: Actor<T>,
isProjectAvailable: boolean, isProjectAvailable: boolean,
hideOnLevel?: SettingsLevel hideOnLevel?: SettingsLevel
): CommandArgument<SettingsLevel, T> => ({ ): CommandArgument<SettingsLevel, T> => ({
@ -55,7 +55,7 @@ interface CreateSettingsArgs {
type: SettingsPaths type: SettingsPaths
send: Function send: Function
context: ContextFrom<typeof settingsMachine> context: ContextFrom<typeof settingsMachine>
actor: InterpreterFrom<typeof settingsMachine> actor: Actor<typeof settingsMachine>
isProjectAvailable: boolean isProjectAvailable: boolean
} }
@ -132,7 +132,7 @@ export function createSettingsCommand({
if (data !== undefined && data !== null) { if (data !== undefined && data !== null) {
send({ type: `set.${type}`, data }) send({ type: `set.${type}`, data })
} else { } else {
send(type) send({ type })
} }
}, },
args: { args: {

View File

@ -1,11 +1,6 @@
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { AllMachines } from 'hooks/useStateMachineCommands' import { AllMachines } from 'hooks/useStateMachineCommands'
import { import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
AnyStateMachine,
ContextFrom,
EventFrom,
InterpreterFrom,
} from 'xstate'
import { Selection } from './selections' import { Selection } from './selections'
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm' import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
@ -186,7 +181,7 @@ export type CommandArgument<
machineContext?: ContextFrom<T> machineContext?: ContextFrom<T>
) => boolean) ) => boolean)
skip?: boolean skip?: boolean
machineActor: InterpreterFrom<T> machineActor: Actor<T>
/** For showing a summary display of the current value, such as in /** For showing a summary display of the current value, such as in
* the command bar's header * the command bar's header
*/ */

View File

@ -2,7 +2,7 @@ import {
AnyStateMachine, AnyStateMachine,
ContextFrom, ContextFrom,
EventFrom, EventFrom,
InterpreterFrom, Actor,
StateFrom, StateFrom,
} from 'xstate' } from 'xstate'
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
@ -23,7 +23,7 @@ interface CreateMachineCommandProps<
groupId: T['id'] groupId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: Actor<T>
commandBarConfig?: StateMachineCommandSetConfig<T, S> commandBarConfig?: StateMachineCommandSetConfig<T, S>
onCancel?: () => void onCancel?: () => void
} }
@ -90,9 +90,9 @@ export function createMachineCommand<
needsReview: commandConfig.needsReview || false, needsReview: commandConfig.needsReview || false,
onSubmit: (data?: S[typeof type]) => { onSubmit: (data?: S[typeof type]) => {
if (data !== undefined && data !== null) { if (data !== undefined && data !== null) {
send(type, { data }) send({ type, data })
} else { } else {
send(type) send({ type })
} }
}, },
} }
@ -124,7 +124,7 @@ function buildCommandArguments<
>( >(
state: StateFrom<T>, state: StateFrom<T>,
args: CommandConfig<T, CommandName, S>['args'], args: CommandConfig<T, CommandName, S>['args'],
machineActor: InterpreterFrom<T> machineActor: Actor<T>
): NonNullable<Command<T, CommandName, S>['args']> { ): NonNullable<Command<T, CommandName, S>['args']> {
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']> const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
@ -143,7 +143,7 @@ export function buildCommandArgument<
>( >(
arg: CommandArgumentConfig<O, T>, arg: CommandArgumentConfig<O, T>,
context: ContextFrom<T>, context: ContextFrom<T>,
machineActor: InterpreterFrom<T> machineActor: Actor<T>
): CommandArgument<O, T> & { inputType: typeof arg.inputType } { ): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
const baseCommandArgument = { const baseCommandArgument = {
description: arg.description, description: arg.description,

View File

@ -1,5 +1,7 @@
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
import { components } from './machine-api' import { components } from './machine-api'
import { reportRejection } from './trap'
import { toSync } from './utils'
export type MachinesListing = Array< export type MachinesListing = Array<
components['schemas']['MachineInfoResponse'] components['schemas']['MachineInfoResponse']
@ -17,7 +19,7 @@ export class MachineManager {
return return
} }
this.updateMachines() this.updateMachines().catch(reportRejection)
} }
start() { start() {
@ -31,11 +33,14 @@ export class MachineManager {
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
const timeoutLoop = () => { const timeoutLoop = () => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(async () => { timeoutId = setTimeout(
await this.updateMachineApiIp() toSync(async () => {
await this.updateMachines() await this.updateMachineApiIp()
timeoutLoop() await this.updateMachines()
}, 10000) timeoutLoop()
}, reportRejection),
10000
)
} }
timeoutLoop() timeoutLoop()
} }

View File

@ -1,12 +1,15 @@
import { MouseEventHandler } from 'react' import { MouseEventHandler } from 'react'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { reportRejection } from './trap'
export const openExternalBrowserIfDesktop = (to?: string) => export const openExternalBrowserIfDesktop = (to?: string) =>
function (e) { function (e) {
if (isDesktop()) { if (isDesktop()) {
// Ignoring because currentTarget could be a few different things // Ignoring because currentTarget could be a few different things
// @ts-ignore // @ts-ignore
window.electron.openExternal(to || e.currentTarget?.href) window.electron
.openExternal(to || e.currentTarget?.href)
.catch(reportRejection)
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
return false return false

View File

@ -16,6 +16,8 @@ import { isDesktop } from 'lib/isDesktop'
import { useRef } from 'react' import { useRef } from 'react'
import { CustomIcon } from 'components/CustomIcon' import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
/** /**
* A setting that can be set at the user or project level * A setting that can be set at the user or project level
@ -206,7 +208,7 @@ export function createSettings() {
ref={inputRef} ref={inputRef}
/> />
<button <button
onClick={async () => { onClick={toSync(async () => {
// In desktop end-to-end tests we can't control the file picker, // In desktop end-to-end tests we can't control the file picker,
// so we seed the new directory value in the element's dataset // so we seed the new directory value in the element's dataset
const inputRefVal = inputRef.current?.dataset.testValue const inputRefVal = inputRef.current?.dataset.testValue
@ -225,7 +227,7 @@ export function createSettings() {
if (newPath.canceled) return if (newPath.canceled) return
updateValue(newPath.filePaths[0]) updateValue(newPath.filePaths[0])
} }
}} }, reportRejection)}
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20" className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
data-testid="project-directory-button" data-testid="project-directory-button"
> >

View File

@ -7,7 +7,8 @@ import { EngineCommand } from 'lang/std/artifactGraph'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { err } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { toSync } from './utils'
type WebSocketResponse = Models['WebSocketResponse_type'] type WebSocketResponse = Models['WebSocketResponse_type']
@ -85,6 +86,7 @@ export async function enginelessExecutor(
setIsStreamReady: () => {}, setIsStreamReady: () => {},
setMediaStream: () => {}, setMediaStream: () => {},
}) as any as EngineCommandManager }) as any as EngineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises
mockEngineCommandManager.startNewSession() mockEngineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, mockEngineCommandManager, true) const programMemory = await _executor(ast, pm, mockEngineCommandManager, true)
await mockEngineCommandManager.waitForAllCommands() await mockEngineCommandManager.waitForAllCommands()
@ -112,7 +114,8 @@ export async function executor(
return new Promise((resolve) => { return new Promise((resolve) => {
engineCommandManager.addEventListener( engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady, EngineCommandManagerEvents.SceneReady,
async () => { toSync(async () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const programMemory = await _executor( const programMemory = await _executor(
ast, ast,
@ -121,8 +124,8 @@ export async function executor(
false false
) )
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
Promise.resolve(programMemory) resolve(programMemory)
} }, reportRejection)
) )
}) })
} }

View File

@ -6,7 +6,7 @@ import {
import { VITE_KC_API_BASE_URL } from 'env' import { VITE_KC_API_BASE_URL } from 'env'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { FILE_EXT } from './constants' import { FILE_EXT } from './constants'
import { ContextFrom, EventData, EventFrom } from 'xstate' import { ContextFrom, EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine' import { fileMachine } from 'machines/fileMachine'
import { NavigateFunction } from 'react-router-dom' import { NavigateFunction } from 'react-router-dom'
import crossPlatformFetch from './crossPlatformFetch' import crossPlatformFetch from './crossPlatformFetch'
@ -14,6 +14,8 @@ import { isDesktop } from 'lib/isDesktop'
import { Themes } from './theme' import { Themes } from './theme'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { getNextFileName } from './desktopFS' import { getNextFileName } from './desktopFS'
import { reportRejection } from './trap'
import { toSync } from './utils'
export async function submitTextToCadPrompt( export async function submitTextToCadPrompt(
prompt: string, prompt: string,
@ -63,12 +65,12 @@ interface TextToKclProps {
trimmedPrompt: string trimmedPrompt: string
fileMachineSend: ( fileMachineSend: (
type: EventFrom<typeof fileMachine>, type: EventFrom<typeof fileMachine>,
data?: EventData data?: unknown
) => unknown ) => unknown
navigate: NavigateFunction navigate: NavigateFunction
commandBarSend: ( commandBarSend: (
type: EventFrom<typeof commandBarMachine>, type: EventFrom<typeof commandBarMachine>,
data?: EventData data?: unknown
) => unknown ) => unknown
context: ContextFrom<typeof fileMachine> context: ContextFrom<typeof fileMachine>
token?: string token?: string
@ -128,37 +130,42 @@ export async function submitAndAwaitTextToKcl({
// Check the status of the text-to-cad API job // Check the status of the text-to-cad API job
// until it is completed // until it is completed
const textToCadComplete = new Promise<Models['TextToCad_type']>( const textToCadComplete = new Promise<Models['TextToCad_type']>(
async (resolve, reject) => { (resolve, reject) => {
const value = await textToCadQueued ;(async () => {
if (value instanceof Error) { const value = await textToCadQueued
reject(value) if (value instanceof Error) {
} reject(value)
const MAX_CHECK_TIMEOUT = 3 * 60_000
const CHECK_INTERVAL = 3000
let timeElapsed = 0
const interval = setInterval(async () => {
timeElapsed += CHECK_INTERVAL
if (timeElapsed >= MAX_CHECK_TIMEOUT) {
clearInterval(interval)
reject(new Error('Text-to-CAD API timed out'))
} }
const check = await getTextToCadResult(value.id, token) const MAX_CHECK_TIMEOUT = 3 * 60_000
if (check instanceof Error) { const CHECK_INTERVAL = 3000
clearInterval(interval)
reject(check)
}
if (check instanceof Error || check.status === 'failed') { let timeElapsed = 0
clearInterval(interval) const interval = setInterval(
reject(check) toSync(async () => {
} else if (check.status === 'completed') { timeElapsed += CHECK_INTERVAL
clearInterval(interval) if (timeElapsed >= MAX_CHECK_TIMEOUT) {
resolve(check) clearInterval(interval)
} reject(new Error('Text-to-CAD API timed out'))
}, CHECK_INTERVAL) }
const check = await getTextToCadResult(value.id, token)
if (check instanceof Error) {
clearInterval(interval)
reject(check)
}
if (check instanceof Error || check.status === 'failed') {
clearInterval(interval)
reject(check)
} else if (check.status === 'completed') {
clearInterval(interval)
resolve(check)
}
}, reportRejection),
CHECK_INTERVAL
)
})().catch(reportRejection)
} }
) )

View File

@ -17,7 +17,7 @@ type ToolbarMode = {
} }
export interface ToolbarItemCallbackProps { export interface ToolbarItemCallbackProps {
modelingStateMatches: StateFrom<typeof modelingMachine>['matches'] modelingState: StateFrom<typeof modelingMachine>
modelingSend: (event: EventFrom<typeof modelingMachine>) => void modelingSend: (event: EventFrom<typeof modelingMachine>) => void
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
sketchPathId: string | false sketchPathId: string | false
@ -85,7 +85,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', groupId: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}), }),
disabled: (state) => !state.can('Extrude'), disabled: (state) => !state.can({ type: 'Extrude' }),
icon: 'extrude', icon: 'extrude',
status: 'available', status: 'available',
title: 'Extrude', title: 'Extrude',
@ -156,7 +156,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}), }),
icon: 'fillet3d', icon: 'fillet3d',
status: DEV ? 'available' : 'kcl-only', status: DEV ? 'available' : 'kcl-only',
disabled: (state) => !state.can('Fillet'), disabled: (state) => !state.can({ type: 'Fillet' }),
title: 'Fillet', title: 'Fillet',
hotkey: 'F', hotkey: 'F',
description: 'Round the edges of a 3D solid.', description: 'Round the edges of a 3D solid.',
@ -273,7 +273,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}), }),
disableHotkey: (state) => disableHotkey: (state) =>
!( !(
state.matches('Sketch.SketchIdle') || state.matches({ Sketch: 'SketchIdle' }) ||
state.matches('Sketch no face') state.matches('Sketch no face')
), ),
icon: 'arrowLeft', icon: 'arrowLeft',
@ -287,35 +287,41 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
'break', 'break',
{ {
id: 'line', id: 'line',
onClick: ({ modelingStateMatches: matches, modelingSend }) => onClick: ({ modelingState, modelingSend }) =>
modelingSend({ modelingSend({
type: 'change tool', type: 'change tool',
data: { data: {
tool: !matches('Sketch.Line tool') ? 'line' : 'none', tool: !modelingState.matches({ Sketch: 'Line tool' })
? 'line'
: 'none',
}, },
}), }),
icon: 'line', icon: 'line',
status: 'available', status: 'available',
disabled: (state) => disabled: (state) =>
state.matches('Sketch no face') || state.matches('Sketch no face') ||
state.matches('Sketch.Rectangle tool.Awaiting second corner') || state.matches({
state.matches('Sketch.Circle tool.Awaiting Radius') || Sketch: { 'Rectangle tool': 'Awaiting second corner' },
}) ||
state.matches({
Sketch: { 'Circle tool': 'Awaiting Radius' },
}) ||
isClosedSketch(state.context), isClosedSketch(state.context),
title: 'Line', title: 'Line',
hotkey: (state) => hotkey: (state) =>
state.matches('Sketch.Line tool') ? ['Esc', 'L'] : 'L', state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L',
description: 'Start drawing straight lines', description: 'Start drawing straight lines',
links: [], links: [],
isActive: (state) => state.matches('Sketch.Line tool'), isActive: (state) => state.matches({ Sketch: 'Line tool' }),
}, },
[ [
{ {
id: 'tangential-arc', id: 'tangential-arc',
onClick: ({ modelingStateMatches, modelingSend }) => onClick: ({ modelingState, modelingSend }) =>
modelingSend({ modelingSend({
type: 'change tool', type: 'change tool',
data: { data: {
tool: !modelingStateMatches('Sketch.Tangential arc to') tool: !modelingState.matches({ Sketch: 'Tangential arc to' })
? 'tangentialArc' ? 'tangentialArc'
: 'none', : 'none',
}, },
@ -324,16 +330,20 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available', status: 'available',
disabled: (state) => disabled: (state) =>
(!isEditingExistingSketch(state.context) && (!isEditingExistingSketch(state.context) &&
!state.matches('Sketch.Tangential arc to')) || !state.matches({ Sketch: 'Tangential arc to' })) ||
state.matches('Sketch.Rectangle tool.Awaiting second corner') || state.matches({
state.matches('Sketch.Circle tool.Awaiting Radius') || Sketch: { 'Rectangle tool': 'Awaiting second corner' },
}) ||
state.matches({
Sketch: { 'Circle tool': 'Awaiting Radius' },
}) ||
isClosedSketch(state.context), isClosedSketch(state.context),
title: 'Tangential Arc', title: 'Tangential Arc',
hotkey: (state) => hotkey: (state) =>
state.matches('Sketch.Tangential arc to') ? ['Esc', 'A'] : 'A', state.matches({ Sketch: 'Tangential arc to' }) ? ['Esc', 'A'] : 'A',
description: 'Start drawing an arc tangent to the current segment', description: 'Start drawing an arc tangent to the current segment',
links: [], links: [],
isActive: (state) => state.matches('Sketch.Tangential arc to'), isActive: (state) => state.matches({ Sketch: 'Tangential arc to' }),
}, },
{ {
id: 'three-point-arc', id: 'three-point-arc',
@ -405,11 +415,11 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[ [
{ {
id: 'corner-rectangle', id: 'corner-rectangle',
onClick: ({ modelingStateMatches, modelingSend }) => onClick: ({ modelingState, modelingSend }) =>
modelingSend({ modelingSend({
type: 'change tool', type: 'change tool',
data: { data: {
tool: !modelingStateMatches('Sketch.Rectangle tool') tool: !modelingState.matches({ Sketch: 'Rectangle tool' })
? 'rectangle' ? 'rectangle'
: 'none', : 'none',
}, },
@ -418,13 +428,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available', status: 'available',
disabled: (state) => disabled: (state) =>
!canRectangleOrCircleTool(state.context) && !canRectangleOrCircleTool(state.context) &&
!state.matches('Sketch.Rectangle tool'), !state.matches({ Sketch: 'Rectangle tool' }),
title: 'Corner rectangle', title: 'Corner rectangle',
hotkey: (state) => hotkey: (state) =>
state.matches('Sketch.Rectangle tool') ? ['Esc', 'R'] : 'R', state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
description: 'Start drawing a rectangle', description: 'Start drawing a rectangle',
links: [], links: [],
isActive: (state) => state.matches('Sketch.Rectangle tool'), isActive: (state) => state.matches({ Sketch: 'Rectangle tool' }),
}, },
{ {
id: 'center-rectangle', id: 'center-rectangle',
@ -473,9 +483,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-length', id: 'constraint-length',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain length') && state.can({ type: 'Constrain length' })
state.can('Constrain length')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain length' }), modelingSend({ type: 'Constrain length' }),
@ -490,9 +499,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-angle', id: 'constraint-angle',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain angle') && state.can({ type: 'Constrain angle' })
state.can('Constrain angle')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain angle' }), modelingSend({ type: 'Constrain angle' }),
@ -506,9 +514,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-vertical', id: 'constraint-vertical',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Make segment vertical') && state.can({ type: 'Make segment vertical' })
state.can('Make segment vertical')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Make segment vertical' }), modelingSend({ type: 'Make segment vertical' }),
@ -523,9 +530,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-horizontal', id: 'constraint-horizontal',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Make segment horizontal') && state.can({ type: 'Make segment horizontal' })
state.can('Make segment horizontal')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Make segment horizontal' }), modelingSend({ type: 'Make segment horizontal' }),
@ -540,9 +546,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-parallel', id: 'constraint-parallel',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain parallel') && state.can({ type: 'Constrain parallel' })
state.can('Constrain parallel')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain parallel' }), modelingSend({ type: 'Constrain parallel' }),
@ -556,9 +561,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-equal-length', id: 'constraint-equal-length',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain equal length') && state.can({ type: 'Constrain equal length' })
state.can('Constrain equal length')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain equal length' }), modelingSend({ type: 'Constrain equal length' }),
@ -572,9 +576,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-horizontal-distance', id: 'constraint-horizontal-distance',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain horizontal distance') && state.can({ type: 'Constrain horizontal distance' })
state.can('Constrain horizontal distance')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain horizontal distance' }), modelingSend({ type: 'Constrain horizontal distance' }),
@ -588,9 +591,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-vertical-distance', id: 'constraint-vertical-distance',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain vertical distance') && state.can({ type: 'Constrain vertical distance' })
state.can('Constrain vertical distance')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain vertical distance' }), modelingSend({ type: 'Constrain vertical distance' }),
@ -604,9 +606,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-absolute-x', id: 'constraint-absolute-x',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain ABS X') && state.can({ type: 'Constrain ABS X' })
state.can('Constrain ABS X')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain ABS X' }), modelingSend({ type: 'Constrain ABS X' }),
@ -620,9 +621,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-absolute-y', id: 'constraint-absolute-y',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain ABS Y') && state.can({ type: 'Constrain ABS Y' })
state.can('Constrain ABS Y')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain ABS Y' }), modelingSend({ type: 'Constrain ABS Y' }),
@ -636,9 +636,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-perpendicular-distance', id: 'constraint-perpendicular-distance',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain perpendicular distance') && state.can({ type: 'Constrain perpendicular distance' })
state.can('Constrain perpendicular distance')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain perpendicular distance' }), modelingSend({ type: 'Constrain perpendicular distance' }),
@ -653,9 +652,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-align-horizontal', id: 'constraint-align-horizontal',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain horizontally align') && state.can({ type: 'Constrain horizontally align' })
state.can('Constrain horizontally align')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain horizontally align' }), modelingSend({ type: 'Constrain horizontally align' }),
@ -669,9 +667,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-align-vertical', id: 'constraint-align-vertical',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain vertically align') && state.can({ type: 'Constrain vertically align' })
state.can('Constrain vertically align')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain vertically align' }), modelingSend({ type: 'Constrain vertically align' }),
@ -685,9 +682,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'snap-to-x', id: 'snap-to-x',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain snap to X') && state.can({ type: 'Constrain snap to X' })
state.can('Constrain snap to X')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain snap to X' }), modelingSend({ type: 'Constrain snap to X' }),
@ -701,9 +697,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'snap-to-y', id: 'snap-to-y',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain snap to Y') && state.can({ type: 'Constrain snap to Y' })
state.can('Constrain snap to Y')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain snap to Y' }), modelingSend({ type: 'Constrain snap to Y' }),
@ -717,9 +712,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'constraint-remove', id: 'constraint-remove',
disabled: (state) => disabled: (state) =>
!( !(
state.matches('Sketch.SketchIdle') && state.matches({ Sketch: 'SketchIdle' }) &&
state.nextEvents.includes('Constrain remove constraints') && state.can({ type: 'Constrain remove constraints' })
state.can('Constrain remove constraints')
), ),
onClick: ({ modelingSend }) => onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain remove constraints' }), modelingSend({ type: 'Constrain remove constraints' }),

View File

@ -2,13 +2,22 @@ import toast from 'react-hot-toast'
type ExcludeErr<T> = Exclude<T, Error> type ExcludeErr<T> = Exclude<T, Error>
/**
* This is intentionally *not* exported due to misuse. We'd like to add a lint.
*/
function isErr<T>(value: ExcludeErr<T> | Error): value is Error {
return value instanceof Error
}
// Used to bubble errors up // Used to bubble errors up
export function err<T>(value: ExcludeErr<T> | Error): value is Error { export function err<T>(value: ExcludeErr<T> | Error): value is Error {
if (!(value instanceof Error)) { if (!isErr(value)) {
return false return false
} }
// TODO: Remove this once we have a lint to prevent misuse of this function.
console.error(value) console.error(value)
return true return true
} }
@ -21,7 +30,7 @@ export function cleanErrs<T>(
const argsWOutErr: Array<ExcludeErr<T>> = [] const argsWOutErr: Array<ExcludeErr<T>> = []
const argsWErr: Array<Error> = [] const argsWErr: Array<Error> = []
for (const v of value) { for (const v of value) {
if (err(v)) { if (isErr(v)) {
argsWErr.push(v) argsWErr.push(v)
} else { } else {
argsWOutErr.push(v) argsWOutErr.push(v)
@ -30,9 +39,28 @@ export function cleanErrs<T>(
return [argsWOutErr.length !== value.length, argsWOutErr, argsWErr] return [argsWOutErr.length !== value.length, argsWOutErr, argsWErr]
} }
export function report(
message: string,
{ showToast }: { showToast: boolean } = { showToast: false }
) {
console.error(message)
if (showToast) {
toast.error(message, { id: 'error' })
}
}
/** /**
* Used to report errors to user at a certain point in execution * Report a promise rejection. The type of reason is `any` so that it matches
* @returns boolean * Promise.prototype.catch.
*/
export function reportRejection(reason: any) {
report((reason ?? 'Unknown promise rejection').toString())
}
/**
* Report an error to the user. Trapping is the opposite of propagating an
* error. We should propagate errors in low-level functions and trap at the top
* level.
*/ */
export function trap<T>( export function trap<T>(
value: ExcludeErr<T> | Error, value: ExcludeErr<T> | Error,
@ -41,7 +69,7 @@ export function trap<T>(
suppress?: boolean suppress?: boolean
} }
): value is Error { ): value is Error {
if (!err(value)) { if (!isErr(value)) {
return false return false
} }

View File

@ -98,3 +98,18 @@ export function isEnumMember<T extends Record<string, unknown>>(
export type DeepPartial<T> = { export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
} }
/**
* Replace a function's return type with another type.
*/
export type WithReturnType<F extends (...args: any[]) => any, NewReturn> = (
...args: Parameters<F>
) => NewReturn
/**
* Assert that a function type is async, preserving its parameter types.
*/
export type AsyncFn<F extends (...args: any[]) => any> = WithReturnType<
F,
Promise<unknown>
>

View File

@ -2,6 +2,8 @@ import { SourceRange } from '../lang/wasm'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
import { AnyMachineSnapshot } from 'xstate'
import { AsyncFn } from './types'
export const uuidv4 = v4 export const uuidv4 = v4
@ -105,6 +107,28 @@ export function deferExecution<T>(func: (args: T) => any, wait: number) {
return deferred return deferred
} }
/**
* Wrap an async function so that it can be called in a sync context, catching
* rejections.
*
* It's common to want to run an async function in a sync context, like an event
* handler or callback. But we want to catch errors.
*
* Note: The returned function doesn't block. This isn't magic.
*
* @param onReject This callback type is from Promise.prototype.catch.
*/
export function toSync<F extends AsyncFn<F>>(
fn: F,
onReject: (
reason: any
) => void | PromiseLike<void | null | undefined> | null | undefined
): (...args: Parameters<F>) => void {
return (...args: Parameters<F>) => {
fn(...args).catch(onReject)
}
}
export function getNormalisedCoordinates({ export function getNormalisedCoordinates({
clientX, clientX,
clientY, clientY,
@ -208,3 +232,7 @@ export function isReducedMotion(): boolean {
export function XOR(bool1: boolean, bool2: boolean): boolean { export function XOR(bool1: boolean, bool2: boolean): boolean {
return (bool1 || bool2) && !(bool1 && bool2) return (bool1 || bool2) && !(bool1 && bool2)
} }
export function getActorNextEvents(snapshot: AnyMachineSnapshot) {
return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])]
}

View File

@ -1,4 +1,4 @@
import { createMachine, assign } from 'xstate' import { assign, setup, fromPromise } from 'xstate'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL' import withBaseURL from '../lib/withBaseURL'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
@ -55,79 +55,93 @@ const persistedToken =
localStorage?.getItem(TOKEN_PERSIST_KEY) || localStorage?.getItem(TOKEN_PERSIST_KEY) ||
'' ''
export const authMachine = createMachine<UserContext, Events>( export const authMachine = setup({
{ types: {} as {
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */ context: UserContext
id: 'Auth', events:
initial: 'checkIfLoggedIn', | Events
states: { | {
checkIfLoggedIn: { type: 'xstate.done.actor.check-logged-in'
id: 'check-if-logged-in', output: {
invoke: { user: Models['User_type']
src: 'getUser', token: string
id: 'check-logged-in', }
onDone: [ }
{ },
target: 'loggedIn', actions: {
actions: assign((context, event) => ({ goToIndexPage: () => {},
user: event.data.user, goToSignInPage: () => {},
token: event.data.token || context.token, },
})), actors: {
}, getUser: fromPromise(({ input }: { input: { token?: string } }) =>
], getUser(input)
onError: [ ),
{ },
target: 'loggedOut', }).createMachine({
actions: assign({ /** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */
user: () => undefined, id: 'Auth',
}), initial: 'checkIfLoggedIn',
}, context: {
], token: persistedToken,
}, },
}, states: {
loggedIn: { checkIfLoggedIn: {
entry: ['goToIndexPage'], id: 'check-if-logged-in',
on: { invoke: {
'Log out': { src: 'getUser',
target: 'loggedOut', input: ({ context }) => ({ token: context.token }),
actions: () => { id: 'check-logged-in',
if (isDesktop()) writeTokenFile('') onDone: [
}, {
target: 'loggedIn',
actions: assign(({ context, event }) => ({
user: event.output.user,
token: event.output.token || context.token,
})),
}, },
}, ],
}, onError: [
loggedOut: { {
entry: ['goToSignInPage'], target: 'loggedOut',
on: {
'Log in': {
target: 'checkIfLoggedIn',
actions: assign({ actions: assign({
token: (_, event) => { user: () => undefined,
const token = event.token || ''
return token
},
}), }),
}, },
],
},
},
loggedIn: {
entry: ['goToIndexPage'],
on: {
'Log out': {
target: 'loggedOut',
actions: () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
if (isDesktop()) writeTokenFile('')
},
}, },
}, },
}, },
schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } }, loggedOut: {
predictableActionArguments: true, entry: ['goToSignInPage'],
preserveActionOrder: true, on: {
context: { 'Log in': {
token: persistedToken, target: 'checkIfLoggedIn',
actions: assign({
token: ({ event }) => {
const token = event.token || ''
return token
},
}),
},
},
}, },
}, },
{ schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
actions: {}, })
services: { getUser },
guards: {},
delays: {},
}
)
async function getUser(context: UserContext) { async function getUser(input: { token?: string }) {
const token = await getAndSyncStoredToken(context) const token = await getAndSyncStoredToken(input)
const url = withBaseURL('/user') const url = withBaseURL('/user')
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -156,7 +170,7 @@ async function getUser(context: UserContext) {
}) })
.then((res) => res.json()) .then((res) => res.json())
.catch((err) => console.error('error from Browser getUser', err)) .catch((err) => console.error('error from Browser getUser', err))
: getUserDesktop(context.token ?? '', VITE_KC_API_BASE_URL) : getUserDesktop(input.token ?? '', VITE_KC_API_BASE_URL)
const user = await userPromise const user = await userPromise
@ -193,17 +207,20 @@ function getCookie(cname: string): string | null {
return null return null
} }
async function getAndSyncStoredToken(context: UserContext): Promise<string> { async function getAndSyncStoredToken(input: {
token?: string
}): Promise<string> {
// dev mode // dev mode
if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN
const token = const token =
context.token && context.token !== '' input.token && input.token !== ''
? context.token ? input.token
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || '' : getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
if (token) { if (token) {
// has just logged in, update storage // has just logged in, update storage
localStorage.setItem(TOKEN_PERSIST_KEY, token) localStorage.setItem(TOKEN_PERSIST_KEY, token)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
isDesktop() && writeTokenFile(token) isDesktop() && writeTokenFile(token)
return token return token
} }

View File

@ -1,4 +1,4 @@
import { assign, createMachine } from 'xstate' import { assign, fromPromise, setup } from 'xstate'
import { import {
Command, Command,
CommandArgument, CommandArgument,
@ -25,7 +25,7 @@ export type CommandBarMachineEvent =
data: { command: Command; argDefaultValues?: { [x: string]: unknown } } data: { command: Command; argDefaultValues?: { [x: string]: unknown } }
} }
| { type: 'Deselect command' } | { type: 'Deselect command' }
| { type: 'Submit command'; data: { [x: string]: unknown } } | { type: 'Submit command'; output: { [x: string]: unknown } }
| { | {
type: 'Add argument' type: 'Add argument'
data: { argument: CommandArgumentWithName<unknown> } data: { argument: CommandArgumentWithName<unknown> }
@ -48,12 +48,16 @@ export type CommandBarMachineEvent =
} }
| { type: 'Submit argument'; data: { [x: string]: unknown } } | { type: 'Submit argument'; data: { [x: string]: unknown } }
| { | {
type: 'done.invoke.validateArguments' type: 'xstate.done.actor.validateSingleArgument'
data: { [x: string]: unknown } output: { [x: string]: unknown }
} }
| { | {
type: 'error.platform.validateArguments' type: 'xstate.done.actor.validateArguments'
data: { message: string; arg: CommandArgumentWithName<unknown> } output: { [x: string]: unknown }
}
| {
type: 'xstate.error.actor.validateArguments'
error: { message: string; arg: CommandArgumentWithName<unknown> }
} }
| { | {
type: 'Find and select command' type: 'Find and select command'
@ -68,382 +72,199 @@ export type CommandBarMachineEvent =
data: { [x: string]: CommandArgumentWithName<unknown> } data: { [x: string]: CommandArgumentWithName<unknown> }
} }
export const commandBarMachine = createMachine( export const commandBarMachine = setup({
{ types: {
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */ context: {} as CommandBarContext,
predictableActionArguments: true, events: {} as CommandBarMachineEvent,
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
context: {
commands: [],
selectedCommand: undefined,
currentArgument: undefined,
selectionRanges: {
otherSelections: [],
codeBasedSelections: [],
},
argumentsToSubmit: {},
} as CommandBarContext,
id: 'Command Bar',
initial: 'Closed',
states: {
Closed: {
on: {
Open: {
target: 'Selecting command',
},
'Find and select command': {
target: 'Command selected',
actions: [
'Find and select command',
'Initialize arguments to submit',
],
},
'Add commands': {
target: 'Closed',
actions: [
assign({
commands: (context, event) =>
[...context.commands, ...event.data.commands].sort(
sortCommands
),
}),
],
internal: true,
},
'Remove commands': {
target: 'Closed',
actions: [
assign({
commands: (context, event) =>
context.commands.filter(
(c) =>
!event.data.commands.some(
(c2) => c2.name === c.name && c2.groupId === c.groupId
)
),
}),
],
internal: true,
},
},
},
'Selecting command': {
on: {
'Select command': {
target: 'Command selected',
actions: ['Set selected command', 'Initialize arguments to submit'],
},
},
},
'Command selected': {
always: [
{
target: 'Closed',
cond: 'Command has no arguments',
actions: ['Execute command'],
},
{
target: 'Checking Arguments',
cond: 'All arguments are skippable',
},
{
target: 'Gathering arguments',
actions: ['Set current argument to first non-skippable'],
},
],
},
'Gathering arguments': {
states: {
'Awaiting input': {
on: {
'Submit argument': {
target: 'Validating',
},
},
},
Validating: {
invoke: {
src: 'Validate argument',
id: 'validateArgument',
onDone: {
target: '#Command Bar.Checking Arguments',
actions: [
assign({
argumentsToSubmit: (context, event) => {
const [argName, argData] = Object.entries(event.data)[0]
const { currentArgument } = context
if (!currentArgument) return {}
return {
...context.argumentsToSubmit,
[argName]: argData,
}
},
}),
],
},
onError: [
{
target: 'Awaiting input',
},
],
},
},
},
initial: 'Awaiting input',
on: {
'Change current argument': {
target: 'Gathering arguments',
internal: true,
actions: ['Set current argument'],
},
'Deselect command': {
target: 'Selecting command',
actions: [
assign({
selectedCommand: (_c, _e) => undefined,
}),
],
},
},
},
Review: {
entry: ['Clear current argument'],
on: {
'Submit command': {
target: 'Closed',
actions: ['Execute command'],
},
'Add argument': {
target: 'Gathering arguments',
actions: ['Set current argument'],
},
'Remove argument': {
target: 'Review',
actions: ['Remove argument'],
},
'Edit argument': {
target: 'Gathering arguments',
actions: ['Set current argument'],
},
},
},
'Checking Arguments': {
invoke: {
src: 'Validate all arguments',
id: 'validateArguments',
onDone: [
{
target: 'Review',
cond: 'Command needs review',
},
{
target: 'Closed',
actions: 'Execute command',
},
],
onError: [
{
target: 'Gathering arguments',
actions: ['Set current argument to first non-skippable'],
},
],
},
},
},
on: {
Close: {
target: '.Closed',
},
Clear: {
target: '#Command Bar',
internal: true,
actions: ['Clear argument data'],
},
},
schema: {
events: {} as CommandBarMachineEvent,
},
preserveActionOrder: true,
}, },
{ actions: {
actions: { enqueueValidArgsToSubmit: assign({
'Execute command': (context, event) => { argumentsToSubmit: ({ context, event }) => {
const { selectedCommand } = context if (event.type !== 'xstate.done.actor.validateSingleArgument') return {}
if (!selectedCommand) return const [argName, argData] = Object.entries(event.output)[0]
if ( const { currentArgument } = context
(selectedCommand?.args && event.type === 'Submit command') || if (!currentArgument) return {}
event.type === 'done.invoke.validateArguments' return {
) { ...context.argumentsToSubmit,
const resolvedArgs = {} as { [x: string]: unknown } [argName]: argData,
for (const [argName, argValue] of Object.entries(
getCommandArgumentKclValuesOnly(event.data)
)) {
resolvedArgs[argName] =
typeof argValue === 'function' ? argValue(context) : argValue
}
selectedCommand?.onSubmit(resolvedArgs)
} else {
selectedCommand?.onSubmit()
} }
}, },
'Set current argument to first non-skippable': assign({ }),
currentArgument: (context, event) => { 'Execute command': ({ context, event }) => {
const { selectedCommand } = context const { selectedCommand } = context
if (!(selectedCommand && selectedCommand.args)) return undefined if (!selectedCommand) return
const rejectedArg = 'data' in event && event.data.arg if (
(selectedCommand?.args && event.type === 'Submit command') ||
event.type === 'xstate.done.actor.validateArguments'
) {
const resolvedArgs = {} as { [x: string]: unknown }
for (const [argName, argValue] of Object.entries(
getCommandArgumentKclValuesOnly(event.output)
)) {
resolvedArgs[argName] =
typeof argValue === 'function' ? argValue(context) : argValue
}
selectedCommand?.onSubmit(resolvedArgs)
} else {
selectedCommand?.onSubmit()
}
},
'Set current argument to first non-skippable': assign({
currentArgument: ({ context, event }) => {
const { selectedCommand } = context
if (!(selectedCommand && selectedCommand.args)) return undefined
const rejectedArg =
'data' in event && 'arg' in event.data && event.data.arg
// Find the first argument that is not to be skipped: // Find the first argument that is not to be skipped:
// that is, the first argument that is not already in the argumentsToSubmit // that is, the first argument that is not already in the argumentsToSubmit
// or that is not undefined, or that is not marked as "skippable". // or that is not undefined, or that is not marked as "skippable".
// TODO validate the type of the existing arguments // TODO validate the type of the existing arguments
let argIndex = 0 let argIndex = 0
while (argIndex < Object.keys(selectedCommand.args).length) { while (argIndex < Object.keys(selectedCommand.args).length) {
const [argName, argConfig] = Object.entries(selectedCommand.args)[ const [argName, argConfig] = Object.entries(selectedCommand.args)[
argIndex argIndex
] ]
const argIsRequired = const argIsRequired =
typeof argConfig.required === 'function' typeof argConfig.required === 'function'
? argConfig.required(context) ? argConfig.required(context)
: argConfig.required : argConfig.required
const mustNotSkipArg = const mustNotSkipArg =
argIsRequired && argIsRequired &&
(!context.argumentsToSubmit.hasOwnProperty(argName) || (!context.argumentsToSubmit.hasOwnProperty(argName) ||
context.argumentsToSubmit[argName] === undefined || context.argumentsToSubmit[argName] === undefined ||
(rejectedArg && rejectedArg.name === argName)) (rejectedArg &&
typeof rejectedArg === 'object' &&
'name' in rejectedArg &&
rejectedArg.name === argName))
if ( if (
mustNotSkipArg === true || mustNotSkipArg === true ||
argIndex + 1 === Object.keys(selectedCommand.args).length argIndex + 1 === Object.keys(selectedCommand.args).length
) { ) {
// If we have reached the end of the arguments and none are skippable, // If we have reached the end of the arguments and none are skippable,
// return the last argument. // return the last argument.
return { return {
...selectedCommand.args[argName], ...selectedCommand.args[argName],
name: argName, name: argName,
}
} }
argIndex++
} }
argIndex++
}
// TODO: use an XState service to continue onto review step // TODO: use an XState service to continue onto review step
// if all arguments are skippable and contain values. // if all arguments are skippable and contain values.
return undefined return undefined
},
}),
'Clear current argument': assign({
currentArgument: undefined,
}),
'Remove argument': assign({
argumentsToSubmit: (context, event) => {
if (event.type !== 'Remove argument') return context.argumentsToSubmit
const argToRemove = Object.values(event.data)[0]
// Extract all but the argument to remove and return it
const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
return rest
},
}),
'Set current argument': assign({
currentArgument: (context, event) => {
switch (event.type) {
case 'Edit argument':
return event.data.arg
case 'Change current argument':
return Object.values(event.data)[0]
default:
return context.currentArgument
}
},
}),
'Clear argument data': assign({
selectedCommand: undefined,
currentArgument: undefined,
argumentsToSubmit: {},
}),
'Set selected command': assign({
selectedCommand: (c, e) =>
e.type === 'Select command' ? e.data.command : c.selectedCommand,
}),
'Find and select command': assign({
selectedCommand: (c, e) => {
if (e.type !== 'Find and select command') return c.selectedCommand
const found = c.commands.find(
(cmd) => cmd.name === e.data.name && cmd.groupId === e.data.groupId
)
return !!found ? found : c.selectedCommand
},
}),
'Initialize arguments to submit': assign({
argumentsToSubmit: (c, e) => {
const command =
'command' in e.data ? e.data.command : c.selectedCommand
if (!command?.args) return {}
const args: { [x: string]: unknown } = {}
for (const [argName, arg] of Object.entries(command.args)) {
args[argName] =
e.data.argDefaultValues && argName in e.data.argDefaultValues
? e.data.argDefaultValues[argName]
: arg.skip && 'defaultValue' in arg
? arg.defaultValue
: undefined
}
return args
},
}),
},
guards: {
'Command needs review': (context, _) =>
context.selectedCommand?.needsReview || false,
},
services: {
'Validate argument': (context, event) => {
if (event.type !== 'Submit argument') return Promise.reject()
return new Promise((resolve, reject) => {
// TODO: figure out if we should validate argument data here or in the form itself,
// and if we should support people configuring a argument's validation function
resolve(event.data)
})
}, },
'Validate all arguments': (context, _) => { }),
'Clear current argument': assign({
currentArgument: undefined,
}),
'Remove argument': assign({
argumentsToSubmit: ({ context, event }) => {
if (event.type !== 'Remove argument') return context.argumentsToSubmit
const argToRemove = Object.values(event.data)[0]
// Extract all but the argument to remove and return it
const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
return rest
},
}),
'Set current argument': assign({
currentArgument: ({ context, event }) => {
switch (event.type) {
case 'Edit argument':
return event.data.arg
case 'Change current argument':
return Object.values(event.data)[0]
default:
return context.currentArgument
}
},
}),
'Clear argument data': assign({
selectedCommand: undefined,
currentArgument: undefined,
argumentsToSubmit: {},
}),
'Set selected command': assign({
selectedCommand: ({ context, event }) =>
event.type === 'Select command'
? event.data.command
: context.selectedCommand,
}),
'Find and select command': assign({
selectedCommand: ({ context, event }) => {
if (event.type !== 'Find and select command')
return context.selectedCommand
const found = context.commands.find(
(cmd) =>
cmd.name === event.data.name && cmd.groupId === event.data.groupId
)
return !!found ? found : context.selectedCommand
},
}),
'Initialize arguments to submit': assign({
argumentsToSubmit: ({ context, event }) => {
if (
event.type !== 'Select command' &&
event.type !== 'Find and select command'
)
return {}
const command =
'data' in event && 'command' in event.data
? event.data.command
: context.selectedCommand
if (!command?.args) return {}
const args: { [x: string]: unknown } = {}
for (const [argName, arg] of Object.entries(command.args)) {
args[argName] =
event.data.argDefaultValues &&
argName in event.data.argDefaultValues
? event.data.argDefaultValues[argName]
: arg.skip && 'defaultValue' in arg
? arg.defaultValue
: undefined
}
return args
},
}),
},
guards: {
'Command needs review': ({ context }) =>
context.selectedCommand?.needsReview || false,
'Command has no arguments': () => false,
'All arguments are skippable': () => false,
},
actors: {
'Validate argument': fromPromise(({ input }) => {
return new Promise((resolve, reject) => {
// TODO: figure out if we should validate argument data here or in the form itself,
// and if we should support people configuring a argument's validation function
resolve(input)
})
}),
'Validate all arguments': fromPromise(
({ input }: { input: CommandBarContext }) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
for (const [argName, argConfig] of Object.entries( for (const [argName, argConfig] of Object.entries(
context.selectedCommand!.args! input.selectedCommand!.args!
)) { )) {
let arg = context.argumentsToSubmit[argName] let arg = input.argumentsToSubmit[argName]
let argValue = typeof arg === 'function' ? arg(context) : arg let argValue = typeof arg === 'function' ? arg(input) : arg
try { try {
const isRequired = const isRequired =
typeof argConfig.required === 'function' typeof argConfig.required === 'function'
? argConfig.required(context) ? argConfig.required(input)
: argConfig.required : argConfig.required
const resolvedDefaultValue = const resolvedDefaultValue =
'defaultValue' in argConfig 'defaultValue' in argConfig
? typeof argConfig.defaultValue === 'function' ? typeof argConfig.defaultValue === 'function'
? argConfig.defaultValue(context) ? argConfig.defaultValue(input)
: argConfig.defaultValue : argConfig.defaultValue
: undefined : undefined
@ -461,7 +282,7 @@ export const commandBarMachine = createMachine(
!( !(
typeof argConfig.options === 'function' typeof argConfig.options === 'function'
? argConfig.options( ? argConfig.options(
context, input,
argConfig.machineActor.getSnapshot().context argConfig.machineActor.getSnapshot().context
) )
: argConfig.options : argConfig.options
@ -502,13 +323,214 @@ export const commandBarMachine = createMachine(
} }
} }
return resolve(context.argumentsToSubmit) return resolve(input.argumentsToSubmit)
}) })
}
),
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */
context: {
commands: [],
selectedCommand: undefined,
currentArgument: undefined,
selectionRanges: {
otherSelections: [],
codeBasedSelections: [],
},
argumentsToSubmit: {},
},
id: 'Command Bar',
initial: 'Closed',
states: {
Closed: {
on: {
Open: {
target: 'Selecting command',
},
'Find and select command': {
target: 'Command selected',
actions: [
'Find and select command',
'Initialize arguments to submit',
],
},
'Add commands': {
target: 'Closed',
actions: [
assign({
commands: ({ context, event }) =>
[...context.commands, ...event.data.commands].sort(
sortCommands
),
}),
],
reenter: false,
},
'Remove commands': {
target: 'Closed',
actions: [
assign({
commands: ({ context, event }) =>
context.commands.filter(
(c) =>
!event.data.commands.some(
(c2) => c2.name === c.name && c2.groupId === c.groupId
)
),
}),
],
reenter: false,
},
}, },
}, },
delays: {},
} 'Selecting command': {
) on: {
'Select command': {
target: 'Command selected',
actions: ['Set selected command', 'Initialize arguments to submit'],
},
},
},
'Command selected': {
always: [
{
target: 'Closed',
guard: 'Command has no arguments',
actions: ['Execute command'],
},
{
target: 'Checking Arguments',
guard: 'All arguments are skippable',
},
{
target: 'Gathering arguments',
actions: ['Set current argument to first non-skippable'],
},
],
},
'Gathering arguments': {
states: {
'Awaiting input': {
on: {
'Submit argument': {
target: 'Validating',
},
},
},
Validating: {
invoke: {
src: 'Validate argument',
id: 'validateSingleArgument',
input: ({ event }) => {
if (event.type !== 'Submit argument') return {}
return event.data
},
onDone: {
target: '#Command Bar.Checking Arguments',
actions: ['enqueueValidArgsToSubmit'],
},
onError: [
{
target: 'Awaiting input',
},
],
},
},
},
initial: 'Awaiting input',
on: {
'Change current argument': {
target: 'Gathering arguments',
internal: true,
actions: ['Set current argument'],
},
'Deselect command': {
target: 'Selecting command',
actions: [
assign({
selectedCommand: (_c, _e) => undefined,
}),
],
},
},
},
Review: {
entry: ['Clear current argument'],
on: {
'Submit command': {
target: 'Closed',
actions: ['Execute command'],
},
'Add argument': {
target: 'Gathering arguments',
actions: ['Set current argument'],
},
'Remove argument': {
target: 'Review',
actions: ['Remove argument'],
},
'Edit argument': {
target: 'Gathering arguments',
actions: ['Set current argument'],
},
},
},
'Checking Arguments': {
invoke: {
src: 'Validate all arguments',
id: 'validateArguments',
input: ({ context }) => context,
onDone: [
{
target: 'Review',
guard: 'Command needs review',
},
{
target: 'Closed',
actions: 'Execute command',
},
],
onError: [
{
target: 'Gathering arguments',
actions: ['Set current argument to first non-skippable'],
},
],
},
},
},
on: {
Close: {
target: '.Closed',
},
Clear: {
target: '#Command Bar',
reenter: false,
actions: ['Clear argument data'],
},
},
})
function sortCommands(a: Command, b: Command) { function sortCommands(a: Command, b: Command) {
if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2 if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2

View File

@ -1,233 +1,393 @@
import { assign, createMachine } from 'xstate' import { assign, fromPromise, setup } from 'xstate'
import { Project, FileEntry } from 'lib/project' import { Project, FileEntry } from 'lib/project'
export const fileMachine = createMachine( type FileMachineContext = {
{ project: Project
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iD6BgGQxGY1Wikm8gkQM0n2UknERm0qmUw2hOVhTkKJURBxq9TAjXOrW0-k42JueIQBKJw1GujJFOiqkkTE0X3M5mUuiYgxmbPseU5CPRhwAImBMGiDpcxcFumF8dptMp1GZRlqNYYdXo-ml1IlzMkHuZVAZJAY1PrtnCuftkQB5DjHE02wLi3EOqX6QmDWWkiZ-HSKf3gph6ZWqcwJIZRjm7bkmmoAZTAvCwsAtYAITQgWAgqG83a4njkqeuGbu+MkLPU7x+iiGszJfwj4ldTvB6UURh0klrht2-MaZCgWDwpF7XCTpBPJooEEEhTIADcuABrQoEVFgAC054gP5XscP7WpiVzpvak5SnO6hJD8RYfJ8ir4kw06zqoTDiMyTA4WGPz7js8JHvwpCnv+WBATepF3mAnieMO6gcOgjTuMOODqF+ApNH+F6AdeIEXGBto4pBoj4u8GjOiMqjiKMqhJFhfyVqqEbJIoCTkmk3wETG6huCcOC3gc96PuoL7voU3gGb+oGimmdq3GJCARuY8RMroEk6MMihKeWrpYepEZMKMSw6Ua+mnEZOQULR9GeIxzG8KxnjsVZpw2YJdnjqJ4QjK59KaoGLKhh6fyBpIsFgtqKjKuCYW7OalpRZgJnwuZH7qBAnbcbZWIOZKeXqAVyhFT8EbaOYK44f6kjlhYG6FVYNibOyB7wo1rbNZQsUMUxLFsZ13UZRiWUQY5uUakNskjdOY2lZSCAqhV24LlhHpMl89Xwm4eD9tRvKtU+pCvh1DQAbyY5nZKMwqZWwxqMorzltoZUrK6YbOlJoazIoX2FD9f2ngDD5tcDFnqGDAmYLADAin1InndMKrqD85jw8ySPvH8pgulqoYWEjc16HjekCoTjYxXRu2JclqVi1TcCQ-1mYwyzcMRhz6lcw9C56Cz4Yatd05ISLxFbYDZlkx1nGCgrSsM5KTJVgMMmjKkEYmAYfwrOkQ30i8WFSF8mTLTCa2FGb-3RTt8V7UlB02z1mX0xKmZMgu8R6C8YahqYwUow9TqBkNxXiLmUgGEyIsRYZUctSTQMg5ZxzpXbdPgcrUEuW57xYUyXkGD5D2Bhog9aKsLyzQywsbOUXXwAEYeEWAKcTk5P7KH8G+ujhuHDDJTJZ0t2QGsvxrlI2q85fiBhlgMZcQq8+iqDJ3OzAML2qCCqxDEkIsNryK+jMpSV1clIck3xB6ViLIWEwmhXiJF0EYIqptUS3nIpRLaQDHajAqvKCwqxEZTxkIXVChJNTqUDCkB4L9q4t1rkTHI2DMyRAeosIawxFxDESMoLCIsNokUYZgZhUF1IDGdK7LyWgX6TULqCIagYcKSHMAya6VdQ6rTPgTLaC9hKpygnSOY8FA7kj0J6WR0QISzn0J8IYN0tIi0TMcLBHcHZp1wf6cB5UiFZxIdEcEhJKyvQ9BqGSqCuIuL0WvXoHj8HeKSL472E0KrBRfrVRGL9cbWEsEAA */ selectedDirectory: FileEntry
id: 'File machine', itemsBeingRenamed: (FileEntry | string)[]
}
initial: 'Reading files', type FileMachineEvents =
| { type: 'Open file'; data: { name: string } }
| {
type: 'Rename file'
data: { oldName: string; newName: string; isDir: boolean }
}
| {
type: 'Create file'
data: {
name: string
makeDir: boolean
content?: string
silent?: boolean
}
}
| { type: 'Delete file'; data: FileEntry }
| { type: 'Set selected directory'; directory: FileEntry }
| { type: 'navigate'; data: { name: string } }
| {
type: 'xstate.done.actor.read-files'
output: Project
}
| {
type: 'xstate.done.actor.rename-file'
output: {
message: string
oldPath: string
newPath: string
}
}
| {
type: 'xstate.done.actor.create-and-open-file'
output: {
message: string
path: string
}
}
| {
type: 'xstate.done.actor.create-file'
output: {
path: string
}
}
| {
type: 'xstate.done.actor.delete-file'
output: {
message: string
}
}
| { type: 'assign'; data: { [key: string]: any } }
| { type: 'Refresh' }
context: { export const fileMachine = setup({
project: {} as Project, types: {} as {
selectedDirectory: {} as FileEntry, context: FileMachineContext
itemsBeingRenamed: [] as string[], events: FileMachineEvents
}, input: Partial<Pick<FileMachineContext, 'project' | 'selectedDirectory'>>
on: {
assign: {
actions: assign((_, event) => ({
...event.data,
})),
target: '.Reading files',
},
Refresh: '.Reading files',
},
states: {
'Has no files': {
on: {
'Create file': {
target: 'Creating and opening file',
},
},
},
'Has files': {
on: {
'Rename file': {
target: 'Renaming file',
},
'Create file': [
{
target: 'Creating and opening file',
cond: 'Is not silent',
},
'Creating file',
],
'Delete file': {
target: 'Deleting file',
},
'Open file': {
target: 'Opening file',
},
'Set selected directory': {
target: 'Has files',
actions: ['setSelectedDirectory'],
},
},
},
'Creating and opening file': {
invoke: {
id: 'create-and-open-file',
src: 'createAndOpenFile',
onDone: [
{
target: 'Reading files',
actions: [
'createToastSuccess',
'addFileToRenamingQueue',
'navigateToFile',
],
},
],
onError: [
{
target: 'Reading files',
actions: ['toastError'],
},
],
},
},
'Renaming file': {
invoke: {
id: 'rename-file',
src: 'renameFile',
onDone: [
{
target: '#File machine.Reading files',
actions: ['renameToastSuccess'],
cond: 'Name has been changed',
},
'Reading files',
],
onError: [
{
target: '#File machine.Reading files',
actions: ['toastError'],
},
],
},
exit: 'removeFileFromRenamingQueue',
},
'Deleting file': {
invoke: {
id: 'delete-file',
src: 'deleteFile',
onDone: [
{
actions: ['toastSuccess'],
target: '#File machine.Reading files',
},
],
onError: {
actions: ['toastError'],
target: '#File machine.Has files',
},
},
},
'Reading files': {
invoke: {
id: 'read-files',
src: 'readFiles',
onDone: [
{
cond: 'Has at least 1 file',
target: 'Has files',
actions: ['setFiles'],
},
{
target: 'Has no files',
actions: ['setFiles'],
},
],
onError: [
{
target: 'Has no files',
actions: ['toastError'],
},
],
},
},
'Opening file': {
entry: ['navigateToFile'],
},
'Creating file': {
invoke: {
src: 'createFile',
id: 'create-file',
onDone: 'Reading files',
onError: 'Reading files',
},
},
},
schema: {
events: {} as
| { type: 'Open file'; data: { name: string } }
| {
type: 'Rename file'
data: { oldName: string; newName: string; isDir: boolean }
}
| {
type: 'Create file'
data: {
name: string
makeDir: boolean
content?: string
silent?: boolean
}
}
| { type: 'Delete file'; data: FileEntry }
| { type: 'Set selected directory'; data: FileEntry }
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-files'
data: Project
}
| {
type: 'done.invoke.rename-file'
data: {
message: string
oldPath: string
newPath: string
}
}
| {
type: 'done.invoke.create-and-open-file'
data: {
message: string
path: string
}
}
| {
type: 'done.invoke.create-file'
data: {
path: string
}
}
| { type: 'assign'; data: { [key: string]: any } }
| { type: 'Refresh' },
},
predictableActionArguments: true,
preserveActionOrder: true,
tsTypes: {} as import('./fileMachine.typegen').Typegen0,
}, },
{ actions: {
actions: { setFiles: assign(({ event }) => {
setFiles: assign((_, event) => { if (event.type !== 'xstate.done.actor.read-files') return {}
return { project: event.data } return { project: event.output }
}), }),
setSelectedDirectory: assign((_, event) => { setSelectedDirectory: assign(({ event }) => {
return { selectedDirectory: event.data } if (event.type !== 'Set selected directory') return {}
}), return { selectedDirectory: event.directory }
}),
addFileToRenamingQueue: assign({
itemsBeingRenamed: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file')
return context.itemsBeingRenamed
return [...context.itemsBeingRenamed, event.output.path]
},
}),
removeFileFromRenamingQueue: assign({
itemsBeingRenamed: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.rename-file')
return context.itemsBeingRenamed
return context.itemsBeingRenamed.filter(
(path) => path !== event.output.oldPath
)
},
}),
navigateToFile: () => {},
renameToastSuccess: () => {},
createToastSuccess: () => {},
toastSuccess: () => {},
toastError: () => {},
},
guards: {
'Name has been changed': ({ event }) => {
if (event.type !== 'xstate.done.actor.rename-file') return false
return event.output.newPath !== event.output.oldPath
}, },
guards: { 'Has at least 1 file': ({ event }) => {
'Name has been changed': (_, event) => { if (event.type !== 'xstate.done.actor.read-files') return false
return event.data.newPath !== event.data.oldPath return !!event?.output?.children && event.output.children.length > 0
},
'Is not silent': ({ event }) =>
event.type === 'Create file' ? !event.data.silent : false,
},
actors: {
readFiles: fromPromise(({ input }: { input: Project }) =>
Promise.resolve(input)
),
createAndOpenFile: fromPromise(
(_: {
input: {
name: string
makeDir: boolean
selectedDirectory: FileEntry
content: string
}
}) => Promise.resolve({ message: '', path: '' })
),
renameFile: fromPromise(
(_: {
input: {
oldName: string
newName: string
isDir: boolean
selectedDirectory: FileEntry
}
}) => Promise.resolve({ message: '', newPath: '', oldPath: '' })
),
deleteFile: fromPromise(
(_: {
input: { path: string; children: FileEntry[] | null; name: string }
}) => Promise.resolve({ message: '' } as { message: string } | undefined)
),
createFile: fromPromise(
(_: {
input: {
name: string
makeDir: boolean
selectedDirectory: FileEntry
content: string
}
}) => Promise.resolve({ path: '' })
),
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iD6BgGQxGY1Wikm8gkQM0n2UknERm0qmUw2hOVhTkKJURBxq9TAjXOrW0-k42JueIQBKJw1GujJFOiqkkTE0X3M5mUuiYgxmbPseU5CPRhwAImBMGiDpcxcFumF8dptMp1GZRlqNYYdXo-ml1IlzMkHuZVAZJAY1PrtnCuftkQB5DjHE02wLi3EOqX6QmDWWkiZ-HSKf3gph6ZWqcwJIZRjm7bkmmoAZTAvCwsAtYAITQgWAgqG83a4njkqeuGbu+MkLPU7x+iiGszJfwj4ldTvB6UURh0klrht2-MaZCgWDwpF7XCTpBPJooEEEhTIADcuABrQoEVFgAC054gP5XscP7WpiVzpvak5SnO6hJD8RYfJ8ir4kw06zqoTDiMyTA4WGPz7js8JHvwpCnv+WBATepF3mAnieMO6gcOgjTuMOODqF+ApNH+F6AdeIEXGBto4pBoj4u8GjOiMqjiKMqhJFhfyVqqEbJIoCTkmk3wETG6huCcOC3gc96PuoL7voU3gGb+oGimmdq3GJCARuY8RMroEk6MMihKeWrpYepEZMKMSw6Ua+mnEZOQULR9GeIxzG8KxnjsVZpw2YJdnjqJ4QjK59KaoGLKhh6fyBpIsFgtqKjKuCYW7OalpRZgJnwuZH7qBAnbcbZWIOZKeXqAVyhFT8EbaOYK44f6kjlhYG6FVYNibOyB7wo1rbNZQsUMUxLFsZ13UZRiWUQY5uUakNskjdOY2lZSCAqhV24LlhHpMl89Xwm4eD9tRvKtU+pCvh1DQAbyY5nZKMwqZWwxqMorzltoZUrK6YbOlJoazIoX2FD9f2ngDD5tcDFnqGDAmYLADAin1InndMKrqD85jw8ySPvH8pgulqoYWEjc16HjekCoTjYxXRu2JclqVi1TcCQ-1mYwyzcMRhz6lcw9C56Cz4Yatd05ISLxFbYDZlkx1nGCgrSsM5KTJVgMMmjKkEYmAYfwrOkQ30i8WFSF8mTLTCa2FGb-3RTt8V7UlB02z1mX0xKmZMgu8R6C8YahqYwUow9TqBkNxXiLmUgGEyIsRYZUctSTQMg5ZxzpXbdPgcrUEuW57xYUyXkGD5D2Bhog9aKsLyzQywsbOUXXwAEYeEWAKcTk5P7KH8G+ujhuHDDJTJZ0t2QGsvxrlI2q85fiBhlgMZcQq8+iqDJ3OzAML2qCCqxDEkIsNryK+jMpSV1clIck3xB6ViLIWEwmhXiJF0EYIqptUS3nIpRLaQDHajAqvKCwqxEZTxkIXVChJNTqUDCkB4L9q4t1rkTHI2DMyRAeosIawxFxDESMoLCIsNokUYZgZhUF1IDGdK7LyWgX6TULqCIagYcKSHMAya6VdQ6rTPgTLaC9hKpygnSOY8FA7kj0J6WR0QISzn0J8IYN0tIi0TMcLBHcHZp1wf6cB5UiFZxIdEcEhJKyvQ9BqGSqCuIuL0WvXoHj8HeKSL472E0KrBRfrVRGL9cbWEsEAA */
id: 'File machine',
initial: 'Reading files',
context: ({ input }) => {
return {
project: input.project ?? ({} as Project), // TODO: Either make this a flexible type or type this property to allow empty object
selectedDirectory: input.selectedDirectory ?? ({} as FileEntry), // TODO: Either make this a flexible type or type this property to allow empty object
itemsBeingRenamed: [],
}
},
on: {
assign: {
actions: assign(({ event }) => ({
...event.data,
})),
target: '.Reading files',
},
Refresh: '.Reading files',
},
states: {
'Has no files': {
on: {
'Create file': {
target: 'Creating and opening file',
},
}, },
}, },
}
) 'Has files': {
on: {
'Rename file': {
target: 'Renaming file',
},
'Create file': [
{
target: 'Creating and opening file',
guard: 'Is not silent',
},
'Creating file',
],
'Delete file': {
target: 'Deleting file',
},
'Open file': {
target: 'Opening file',
},
'Set selected directory': {
target: 'Has files',
actions: ['setSelectedDirectory'],
},
},
},
'Creating and opening file': {
invoke: {
id: 'create-and-open-file',
src: 'createAndOpenFile',
input: ({ event, context }) => {
if (event.type !== 'Create file')
// This is just to make TS happy
return {
name: '',
makeDir: false,
selectedDirectory: context.selectedDirectory,
content: '',
}
return {
name: event.data.name,
makeDir: event.data.makeDir,
selectedDirectory: context.selectedDirectory,
content: event.data.content ?? '',
}
},
onDone: [
{
target: 'Reading files',
actions: [
{
type: 'createToastSuccess',
params: ({
event,
}: {
// TODO: rely on type inference
event: Extract<
FileMachineEvents,
{ type: 'xstate.done.actor.create-and-open-file' }
>
}) => {
return { message: event.output.message }
},
},
'addFileToRenamingQueue',
'navigateToFile',
],
},
],
onError: [
{
target: 'Reading files',
actions: ['toastError'],
},
],
},
},
'Renaming file': {
invoke: {
id: 'rename-file',
src: 'renameFile',
input: ({ event, context }) => {
if (event.type !== 'Rename file') {
// This is just to make TS happy
return {
oldName: '',
newName: '',
isDir: false,
selectedDirectory: {} as FileEntry,
}
}
return {
oldName: event.data.oldName,
newName: event.data.newName,
isDir: event.data.isDir,
selectedDirectory: context.selectedDirectory,
}
},
onDone: [
{
target: '#File machine.Reading files',
actions: ['renameToastSuccess'],
guard: 'Name has been changed',
},
'Reading files',
],
onError: [
{
target: '#File machine.Reading files',
actions: ['toastError'],
},
],
},
exit: 'removeFileFromRenamingQueue',
},
'Deleting file': {
invoke: {
id: 'delete-file',
src: 'deleteFile',
input: ({ event }) => {
if (event.type !== 'Delete file') {
// This is just to make TS happy
return {
path: '',
children: [],
name: '',
}
}
return {
path: event.data.path,
children: event.data.children,
name: event.data.name,
}
},
onDone: [
{
actions: ['toastSuccess'],
target: '#File machine.Reading files',
},
],
onError: {
actions: ['toastError'],
target: '#File machine.Has files',
},
},
},
'Reading files': {
invoke: {
id: 'read-files',
src: 'readFiles',
input: ({ context }) => context.project,
onDone: [
{
guard: 'Has at least 1 file',
target: 'Has files',
actions: ['setFiles'],
},
{
target: 'Has no files',
actions: ['setFiles'],
},
],
onError: [
{
target: 'Has no files',
actions: ['toastError'],
},
],
},
},
'Opening file': {
entry: ['navigateToFile'],
},
'Creating file': {
invoke: {
src: 'createFile',
id: 'create-file',
input: ({ event, context }) => {
if (event.type !== 'Create file') {
// This is just to make TS happy
return {
name: '',
makeDir: false,
selectedDirectory: {} as FileEntry,
content: '',
}
}
return {
name: event.data.name,
makeDir: event.data.makeDir,
selectedDirectory: context.selectedDirectory,
content: event.data.content ?? '',
}
},
onDone: 'Reading files',
onError: 'Reading files',
},
},
},
})

View File

@ -1,164 +1,233 @@
import { assign, createMachine } from 'xstate' import { assign, fromPromise, setup } from 'xstate'
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig' import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
import { Project } from 'lib/project' import { Project } from 'lib/project'
export const homeMachine = createMachine( export const homeMachine = setup({
{ types: {
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */ context: {} as {
id: 'Home machine', projects: Project[]
defaultProjectName: string
initial: 'Reading projects', defaultDirectory: string
context: {
projects: [] as Project[],
defaultProjectName: '',
defaultDirectory: '',
}, },
events: {} as
on: { | { type: 'Open project'; data: HomeCommandSchema['Open project'] }
assign: { | { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
actions: assign((_, event) => ({ | { type: 'Create project'; data: HomeCommandSchema['Create project'] }
...event.data, | { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
})), | { type: 'navigate'; data: { name: string } }
target: '.Reading projects', | {
}, type: 'xstate.done.actor.read-projects'
output: Project[]
}
| { type: 'assign'; data: { [key: string]: any } },
input: {} as {
projects: Project[]
defaultProjectName: string
defaultDirectory: string
}, },
states: {
'Has no projects': {
on: {
'Create project': {
target: 'Creating project',
},
},
},
'Has projects': {
on: {
'Rename project': {
target: 'Renaming project',
},
'Create project': {
target: 'Creating project',
},
'Delete project': {
target: 'Deleting project',
},
'Open project': {
target: 'Opening project',
},
},
},
'Creating project': {
invoke: {
id: 'create-project',
src: 'createProject',
onDone: [
{
target: 'Reading projects',
actions: ['toastSuccess'],
},
],
onError: [
{
target: 'Reading projects',
actions: ['toastError'],
},
],
},
},
'Renaming project': {
invoke: {
id: 'rename-project',
src: 'renameProject',
onDone: [
{
target: '#Home machine.Reading projects',
actions: ['toastSuccess'],
},
],
onError: [
{
target: '#Home machine.Reading projects',
actions: ['toastError'],
},
],
},
},
'Deleting project': {
invoke: {
id: 'delete-project',
src: 'deleteProject',
onDone: [
{
actions: ['toastSuccess'],
target: '#Home machine.Reading projects',
},
],
onError: {
actions: ['toastError'],
target: '#Home machine.Has projects',
},
},
},
'Reading projects': {
invoke: {
id: 'read-projects',
src: 'readProjects',
onDone: [
{
cond: 'Has at least 1 project',
target: 'Has projects',
actions: ['setProjects'],
},
{
target: 'Has no projects',
actions: ['setProjects'],
},
],
onError: [
{
target: 'Has no projects',
actions: ['toastError'],
},
],
},
},
'Opening project': {
entry: ['navigateToProject'],
},
},
schema: {
events: {} as
| { type: 'Open project'; data: HomeCommandSchema['Open project'] }
| { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
| { type: 'Create project'; data: HomeCommandSchema['Create project'] }
| { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-projects'
data: Project[]
}
| { type: 'assign'; data: { [key: string]: any } },
},
predictableActionArguments: true,
preserveActionOrder: true,
tsTypes: {} as import('./homeMachine.typegen').Typegen0,
}, },
{ actions: {
actions: { setProjects: assign({
setProjects: assign((_, event) => { projects: ({ context, event }) =>
return { projects: event.data as Project[] } 'output' in event ? event.output : context.projects,
}), }),
toastSuccess: () => {},
toastError: () => {},
navigateToProject: () => {},
},
actors: {
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
createProject: fromPromise((_: { input: { name: string } }) =>
Promise.resolve('')
),
renameProject: fromPromise(
(_: {
input: {
oldName: string
newName: string
defaultProjectName: string
defaultDirectory: string
}
}) => Promise.resolve('')
),
deleteProject: fromPromise(
(_: { input: { defaultDirectory: string; name: string } }) =>
Promise.resolve('')
),
},
guards: {
'Has at least 1 project': () => false,
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
id: 'Home machine',
initial: 'Reading projects',
context: {
projects: [],
defaultProjectName: '',
defaultDirectory: '',
},
on: {
assign: {
actions: assign(({ event }) => ({
...event.data,
})),
target: '.Reading projects',
}, },
} },
) states: {
'Has no projects': {
on: {
'Create project': {
target: 'Creating project',
},
},
},
'Has projects': {
on: {
'Rename project': {
target: 'Renaming project',
},
'Create project': {
target: 'Creating project',
},
'Delete project': {
target: 'Deleting project',
},
'Open project': {
target: 'Opening project',
},
},
},
'Creating project': {
invoke: {
id: 'create-project',
src: 'createProject',
input: ({ event }) => {
if (event.type !== 'Create project') {
return {
name: '',
}
}
return {
name: event.data.name,
}
},
onDone: [
{
target: 'Reading projects',
actions: ['toastSuccess'],
},
],
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: '',
}
}
return {
defaultProjectName: context.defaultProjectName,
defaultDirectory: context.defaultDirectory,
oldName: event.data.oldName,
newName: event.data.newName,
}
},
onDone: [
{
target: '#Home machine.Reading projects',
actions: ['toastSuccess'],
},
],
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'],
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'],
},
{
target: 'Has no projects',
actions: ['setProjects'],
},
],
onError: [
{
target: 'Has no projects',
actions: ['toastError'],
},
],
},
},
'Opening project': {
entry: ['navigateToProject'],
},
},
})

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { assign, createMachine } from 'xstate' import { assign, setup } from 'xstate'
import { Themes, getSystemTheme, setThemeClass } from 'lib/theme' import { Themes, getSystemTheme, setThemeClass } from 'lib/theme'
import { createSettings, settings } from 'lib/settings/initialSettings' import { createSettings, settings } from 'lib/settings/initialSettings'
import { import {
@ -9,177 +9,187 @@ import {
WildcardSetEvent, WildcardSetEvent,
} from 'lib/settings/settingsTypes' } from 'lib/settings/settingsTypes'
export const settingsMachine = createMachine( export const settingsMachine = setup({
{ types: {
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
id: 'Settings',
predictableActionArguments: true,
context: {} as ReturnType<typeof createSettings>, context: {} as ReturnType<typeof createSettings>,
initial: 'idle', input: {} as ReturnType<typeof createSettings>,
states: { events: {} as
idle: { | WildcardSetEvent<SettingsPaths>
entry: ['setThemeClass', 'setClientSideSceneUnits'], | SetEventTypes
| {
type: 'set.app.theme'
data: { level: SettingsLevel; value: Themes }
}
| {
type: 'set.modeling.units'
data: { level: SettingsLevel; value: BaseUnit }
}
| { type: 'Reset settings'; defaultDirectory: string }
| { type: 'Set all settings'; settings: typeof settings },
},
actions: {
setEngineTheme: () => {},
setClientTheme: () => {},
'Execute AST': () => {},
toastSuccess: () => {},
setEngineEdges: () => {},
setEngineScaleGridVisibility: () => {},
setClientSideSceneUnits: () => {},
persistSettings: () => {},
resetSettings: assign(({ context, event }) => {
if (!('defaultDirectory' in event)) return {}
// Reset everything except onboarding status,
// which should be preserved
const newSettings = createSettings()
if (context.app.onboardingStatus.user) {
newSettings.app.onboardingStatus.user =
context.app.onboardingStatus.user
}
// We instead pass in the default directory since it's asynchronous
// to re-initialize, and that can be done by the caller.
newSettings.app.projectDirectory.default = event.defaultDirectory
on: { return newSettings
'*': { }),
target: 'persisting settings', setAllSettings: assign(({ event }) => {
actions: ['setSettingAtLevel', 'toastSuccess'], if (!('settings' in event)) return {}
}, return event.settings
}),
setSettingAtLevel: assign(({ context, event }) => {
if (!('data' in event)) return {}
const { level, value } = event.data
const [category, setting] = event.type
.replace(/^set./, '')
.split('.') as [keyof typeof settings, string]
'set.app.onboardingStatus': { // @ts-ignore
target: 'persisting settings', context[category][setting][level] = value
// No toast const newContext = {
actions: ['setSettingAtLevel'], ...context,
}, [category]: {
...context[category],
'set.app.themeColor': { // @ts-ignore
target: 'persisting settings', [setting]: context[category][setting],
// No toast
actions: ['setSettingAtLevel'],
},
'set.modeling.defaultUnit': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setClientSideSceneUnits',
'Execute AST',
],
},
'set.app.theme': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setThemeClass',
'setEngineTheme',
'setClientTheme',
],
},
'set.app.streamIdleMode': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess'],
},
'set.modeling.highlightEdges': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
},
'Reset settings': {
target: 'persisting settings',
actions: [
'resetSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'setClientTheme',
],
},
'Set all settings': {
actions: [
'setAllSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'setClientTheme',
],
},
'set.modeling.showScaleGrid': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineScaleGridVisibility',
],
},
}, },
}, }
'persisting settings': { return newContext
invoke: { }),
src: 'Persist settings', setThemeClass: ({ context }) => {
id: 'persistSettings', const currentTheme = context.app.theme.current ?? Themes.System
onDone: 'idle', setThemeClass(
}, currentTheme === Themes.System ? getSystemTheme() : currentTheme
}, )
},
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {
events: {} as
| WildcardSetEvent<SettingsPaths>
| SetEventTypes
| {
type: 'set.app.theme'
data: { level: SettingsLevel; value: Themes }
}
| {
type: 'set.modeling.units'
data: { level: SettingsLevel; value: BaseUnit }
}
| { type: 'Reset settings'; defaultDirectory: string }
| { type: 'Set all settings'; settings: typeof settings },
}, },
}, },
{ }).createMachine({
actions: { /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
resetSettings: assign((context, { defaultDirectory }) => { id: 'Settings',
// Reset everything except onboarding status, initial: 'idle',
// which should be preserved context: ({ input }) => {
const newSettings = createSettings() return {
if (context.app.onboardingStatus.user) { ...createSettings(),
newSettings.app.onboardingStatus.user = ...input,
context.app.onboardingStatus.user }
} },
// We instead pass in the default directory since it's asynchronous states: {
// to re-initialize, and that can be done by the caller. idle: {
newSettings.app.projectDirectory.default = defaultDirectory entry: ['setThemeClass', 'setClientSideSceneUnits'],
return newSettings on: {
}), '*': {
setAllSettings: assign((_, event) => { target: 'persisting settings',
return event.settings actions: ['setSettingAtLevel', 'toastSuccess'],
}), },
setSettingAtLevel: assign((context, event) => {
const { level, value } = event.data
const [category, setting] = event.type
.replace(/^set./, '')
.split('.') as [keyof typeof settings, string]
// @ts-ignore 'set.app.onboardingStatus': {
context[category][setting][level] = value target: 'persisting settings',
const newContext = { // No toast
...context, actions: ['setSettingAtLevel'],
[category]: { },
...context[category],
// @ts-ignore
[setting]: context[category][setting],
},
}
return newContext 'set.app.themeColor': {
}), target: 'persisting settings',
setThemeClass: (context) => {
const currentTheme = context.app.theme.current ?? Themes.System // No toast
setThemeClass( actions: ['setSettingAtLevel'],
currentTheme === Themes.System ? getSystemTheme() : currentTheme },
)
'set.modeling.defaultUnit': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setClientSideSceneUnits',
'Execute AST',
],
},
'set.app.theme': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setThemeClass',
'setEngineTheme',
'setClientTheme',
],
},
'set.app.streamIdleMode': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess'],
},
'set.modeling.highlightEdges': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
},
'Reset settings': {
target: 'persisting settings',
actions: [
'resetSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'setClientTheme',
],
},
'Set all settings': {
actions: [
'setAllSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'setClientTheme',
],
},
'set.modeling.showScaleGrid': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineScaleGridVisibility',
],
},
}, },
}, },
}
) 'persisting settings': {
entry: ['persistSettings'],
always: 'idle',
},
},
})

View File

@ -19,6 +19,7 @@ import electronUpdater, { type AppUpdater } from 'electron-updater'
import minimist from 'minimist' import minimist from 'minimist'
import getCurrentProjectFile from 'lib/getCurrentProjectFile' import getCurrentProjectFile from 'lib/getCurrentProjectFile'
import os from 'node:os' import os from 'node:os'
import { reportRejection } from 'lib/trap'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
@ -87,28 +88,30 @@ const createWindow = (filePath?: string): BrowserWindow => {
// and load the index.html of the app. // and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
} else { } else {
getProjectPathAtStartup(filePath).then((projectPath) => { getProjectPathAtStartup(filePath)
const startIndex = path.join( .then(async (projectPath) => {
__dirname, const startIndex = path.join(
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html` __dirname,
) `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
)
if (projectPath === null) { if (projectPath === null) {
newWindow.loadFile(startIndex) await newWindow.loadFile(startIndex)
return return
} }
console.log('Loading file', projectPath) console.log('Loading file', projectPath)
const fullUrl = `/file/${encodeURIComponent(projectPath)}` const fullUrl = `/file/${encodeURIComponent(projectPath)}`
console.log('Full URL', fullUrl) console.log('Full URL', fullUrl)
newWindow.loadFile(startIndex, { await newWindow.loadFile(startIndex, {
hash: fullUrl, hash: fullUrl,
})
}) })
}) .catch(reportRejection)
} }
// Open the DevTools. // Open the DevTools.
@ -175,6 +178,7 @@ ipcMain.handle('login', async (event, host) => {
const handle = await client.deviceAuthorization() const handle = await client.deviceAuthorization()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
shell.openExternal(handle.verification_uri_complete) shell.openExternal(handle.verification_uri_complete)
// Wait for the user to login. // Wait for the user to login.
@ -241,12 +245,12 @@ export async function checkForUpdates(autoUpdater: AppUpdater) {
console.log(result) console.log(result)
} }
app.on('ready', async () => { app.on('ready', () => {
const autoUpdater = getAutoUpdater() const autoUpdater = getAutoUpdater()
checkForUpdates(autoUpdater) checkForUpdates(autoUpdater).catch(reportRejection)
const fifteenMinutes = 15 * 60 * 1000 const fifteenMinutes = 15 * 60 * 1000
setInterval(() => { setInterval(() => {
checkForUpdates(autoUpdater) checkForUpdates(autoUpdater).catch(reportRejection)
}, fifteenMinutes) }, fifteenMinutes)
autoUpdater.on('update-available', (info) => { autoUpdater.on('update-available', (info) => {

View File

@ -1,14 +1,17 @@
import { reportRejection } from 'lib/trap'
import { ReportHandler } from 'web-vitals' import { ReportHandler } from 'web-vitals'
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals')
getCLS(onPerfEntry) .then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getFID(onPerfEntry) getCLS(onPerfEntry)
getFCP(onPerfEntry) getFID(onPerfEntry)
getLCP(onPerfEntry) getFCP(onPerfEntry)
getTTFB(onPerfEntry) getLCP(onPerfEntry)
}) getTTFB(onPerfEntry)
})
.catch(reportRejection)
} }
} }

View File

@ -14,7 +14,7 @@ import { type HomeLoaderData } from 'lib/types'
import Loading from 'components/Loading' import Loading from 'components/Loading'
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { homeMachine } from '../machines/homeMachine' import { homeMachine } from '../machines/homeMachine'
import { ContextFrom, EventFrom } from 'xstate' import { fromPromise } from 'xstate'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { import {
getNextSearchParams, getNextSearchParams,
@ -68,95 +68,102 @@ const Home = () => {
) )
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const [state, send, actor] = useMachine(homeMachine, { const [state, send, actor] = useMachine(
context: { homeMachine.provide({
projects: loadedProjects, actions: {
defaultProjectName: settings.projects.defaultProjectName.current, navigateToProject: ({ context, event }) => {
defaultDirectory: settings.app.projectDirectory.current, if ('data' in event && event.data && 'name' in event.data) {
}, let projectPath =
actions: { context.defaultDirectory +
navigateToProject: ( window.electron.path.sep +
context: ContextFrom<typeof homeMachine>, event.data.name
event: EventFrom<typeof homeMachine> onProjectOpen(
) => { {
if (event.data && 'name' in event.data) { name: event.data.name,
let projectPath = path: projectPath,
context.defaultDirectory + },
window.electron.path.sep + null
event.data.name )
onProjectOpen( commandBarSend({ type: 'Close' })
{ navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
name: event.data.name,
path: projectPath,
},
null
)
commandBarSend({ type: 'Close' })
navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
}
},
toastSuccess: (_, event) => toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
},
services: {
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
listProjects(),
createProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Create project'>
) => {
let name = (
event.data && 'name' in event.data
? event.data.name
: settings.projects.defaultProjectName.current
).trim()
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProjectDirectory(name)
return `Successfully created "${name}"`
},
renameProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Rename project'>
) => {
const { oldName, newName } = event.data
let name = newName ? newName : context.defaultProjectName
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await renameProjectDirectory(
window.electron.path.join(context.defaultDirectory, oldName),
name
)
return `Successfully renamed "${oldName}" to "${name}"`
},
deleteProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Delete project'>
) => {
await window.electron.rm(
window.electron.path.join(context.defaultDirectory, event.data.name),
{
recursive: true,
} }
) },
return `Successfully deleted "${event.data.name}"` toastSuccess: ({ event }) =>
toast.success(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
typeof event.output === 'string' &&
event.output) ||
''
),
toastError: ({ event }) =>
toast.error(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
typeof event.output === 'string' &&
event.output) ||
''
),
}, },
}, actors: {
guards: { readProjects: fromPromise(() => listProjects()),
'Has at least 1 project': (_, event: EventFrom<typeof homeMachine>) => { createProject: fromPromise(async ({ input }) => {
if (event.type !== 'done.invoke.read-projects') return false let name = (
return event?.data?.length ? event.data?.length >= 1 : false input && 'name' in input && input.name
? input.name
: settings.projects.defaultProjectName.current
).trim()
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProjectDirectory(name)
return `Successfully created "${name}"`
}),
renameProject: fromPromise(async ({ input }) => {
const { oldName, newName, defaultProjectName, defaultDirectory } =
input
let name = newName ? newName : defaultProjectName
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await renameProjectDirectory(
window.electron.path.join(defaultDirectory, oldName),
name
)
return `Successfully renamed "${oldName}" to "${name}"`
}),
deleteProject: fromPromise(async ({ input }) => {
await window.electron.rm(
window.electron.path.join(input.defaultDirectory, input.name),
{
recursive: true,
}
)
return `Successfully deleted "${input.name}"`
}),
}, },
}, guards: {
}) 'Has at least 1 project': ({ event }) => {
if (event.type !== 'xstate.done.actor.read-projects') return false
console.log(`from has at least 1 project: ${event.output.length}`)
return event.output.length ? event.output.length >= 1 : false
},
},
}),
{
input: {
projects: loadedProjects,
defaultProjectName: settings.projects.defaultProjectName.current,
defaultDirectory: settings.app.projectDirectory.current,
},
}
)
const { projects } = state.context const { projects } = state.context
const [searchParams, setSearchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()
const { searchResults, query, setQuery } = useProjectSearch(projects) const { searchResults, query, setQuery } = useProjectSearch(projects)
@ -197,14 +204,18 @@ const Home = () => {
) )
if (newProjectName !== project.name) { if (newProjectName !== project.name) {
send('Rename project', { send({
data: { oldName: project.name, newName: newProjectName }, type: 'Rename project',
data: { oldName: project.name, newName: newProjectName as string },
}) })
} }
} }
async function handleDeleteProject(project: Project) { async function handleDeleteProject(project: Project) {
send('Delete project', { data: { name: project.name || '' } }) send({
type: 'Delete project',
data: { name: project.name || '' },
})
} }
return ( return (
@ -217,7 +228,9 @@ const Home = () => {
<h1 className="text-3xl font-bold">Your Projects</h1> <h1 className="text-3xl font-bold">Your Projects</h1>
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => send('Create project')} onClick={() =>
send({ type: 'Create project', data: { name: '' } })
}
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15" className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
iconStart={{ iconStart={{
icon: 'plus', icon: 'plus',

View File

@ -13,6 +13,7 @@ export default function FutureWork() {
useDemoCode() useDemoCode()
useEffect(() => { useEffect(() => {
send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneInfra.camControls.resetCameraPosition() sceneInfra.camControls.resetCameraPosition()
}, [send]) }, [send])

View File

@ -13,6 +13,8 @@ import { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
/** /**
* Show either a welcome screen or a warning screen * Show either a welcome screen or a warning screen
@ -80,7 +82,7 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
<OnboardingButtons <OnboardingButtons
className="mt-6" className="mt-6"
dismiss={dismiss} dismiss={dismiss}
next={onAccept} next={toSync(onAccept, reportRejection)}
nextText="Make a new project" nextText="Make a new project"
/> />
</> </>
@ -102,14 +104,14 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
<OnboardingButtons <OnboardingButtons
className="mt-6" className="mt-6"
dismiss={dismiss} dismiss={dismiss}
next={async () => { next={toSync(async () => {
// We do want to update both the state and editor here. // We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket) codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile() await codeManager.writeToFile()
await kclManager.executeCode(true) await kclManager.executeCode(true)
props.setShouldShowWarning(false) props.setShouldShowWarning(false)
}} }, reportRejection)}
nextText="Overwrite code and continue" nextText="Overwrite code and continue"
/> />
</> </>

View File

@ -16,6 +16,7 @@ export default function Sketching() {
await kclManager.executeCode(true) await kclManager.executeCode(true)
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
clearEditor() clearEditor()
}, []) }, [])

View File

@ -21,6 +21,8 @@ import { ActionButton } from 'components/ActionButton'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { codeManager, editorManager, kclManager } from 'lib/singletons' import { codeManager, editorManager, kclManager } from 'lib/singletons'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
export const kbdClasses = export const kbdClasses =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2' 'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
@ -80,11 +82,13 @@ export const onboardingRoutes = [
export function useDemoCode() { export function useDemoCode() {
useEffect(() => { useEffect(() => {
if (!editorManager.editorView || codeManager.code === bracket) return if (!editorManager.editorView || codeManager.code === bracket) return
setTimeout(async () => { setTimeout(
codeManager.updateCodeStateEditor(bracket) toSync(async () => {
await kclManager.executeCode(true) codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile() await kclManager.executeCode(true)
}) await codeManager.writeToFile()
}, reportRejection)
)
}, [editorManager.editorView]) }, [editorManager.editorView])
} }

View File

@ -11,6 +11,8 @@ import { CustomIcon } from 'components/CustomIcon'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { APP_VERSION } from './Settings' import { APP_VERSION } from './Settings'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
const subtleBorder = const subtleBorder =
'border border-solid border-chalkboard-30 dark:border-chalkboard-80' 'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
@ -104,7 +106,7 @@ const SignIn = () => {
</p> </p>
{isDesktop() ? ( {isDesktop() ? (
<button <button
onClick={signInDesktop} onClick={toSync(signInDesktop, reportRejection)}
className={ className={
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' + 'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15' '!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'

View File

@ -2952,13 +2952,13 @@
"@babel/types" "^7.21.4" "@babel/types" "^7.21.4"
recast "^0.23.1" recast "^0.23.1"
"@xstate/react@^3.2.2": "@xstate/react@^4.1.1":
version "3.2.2" version "4.1.1"
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa"
integrity sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ== integrity sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA==
dependencies: dependencies:
use-isomorphic-layout-effect "^1.1.2" use-isomorphic-layout-effect "^1.1.2"
use-sync-external-store "^1.0.0" use-sync-external-store "^1.2.0"
"@xstate/tools-shared@^4.1.0": "@xstate/tools-shared@^4.1.0":
version "4.1.0" version "4.1.0"
@ -8757,16 +8757,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -8860,14 +8851,7 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -9412,7 +9396,7 @@ use-latest@^1.2.1:
dependencies: dependencies:
use-isomorphic-layout-effect "^1.1.1" use-isomorphic-layout-effect "^1.1.1"
use-sync-external-store@^1.0.0: use-sync-external-store@^1.2.0:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
@ -9741,16 +9725,7 @@ word-wrap@^1.2.3, word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -9793,11 +9768,16 @@ xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1:
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.0.0-beta.54.tgz#d80f1a9e43ad883a65fc9b399161bd39633bd9bf" resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.0.0-beta.54.tgz#d80f1a9e43ad883a65fc9b399161bd39633bd9bf"
integrity sha512-BTnCPBQ2iTKe4uCnHEe1hNx6VTbXU+5mQGybSQHOjTLiBi4Ryi+tL9T6N1tmqagvM8rfl4XRfvndogfWCWcdpw== integrity sha512-BTnCPBQ2iTKe4uCnHEe1hNx6VTbXU+5mQGybSQHOjTLiBi4Ryi+tL9T6N1tmqagvM8rfl4XRfvndogfWCWcdpw==
xstate@^4.33.4, xstate@^4.38.2: xstate@^4.33.4:
version "4.38.3" version "4.38.3"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075" resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw== integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==
xstate@^5.17.4:
version "5.17.4"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.17.4.tgz#334ab2da123973634097f7ca48387ae1589c774e"
integrity sha512-KM2FYVOUJ04HlOO4TY3wEXqoYPR/XsDu+ewm+IWw0vilXqND0jVfvv04tEFwp8Mkk7I/oHXM8t1Ex9xJyUS4ZA==
xterm-addon-fit@^0.5.0: xterm-addon-fit@^0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596" resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"