Migrate to XState v5 (#3735)
* migrate settingsMachine
* Guard events with properties instead
* migrate settingsMachine
* Migrate auth machine
* Migrate file machine
* Migrate depracated types
* Migrate home machine
* Migrate command bar machine
* Version fixes
* Migrate command bar machine
* Migrate modeling machine
* Migrate types, state.can, state.matches and state.nextEvents
* Fix syntax
* Pass in modelingState into editor manager instead of modeling event
* Fix issue with missing command bar provider
* Fix state transition
* Fix type issue in Home
* Make sure no guards rely on event type
* Fix up command bar submission logic
* Home machine tweaks to get things running
* Fix AST fillet function args
* Handle "Set selection" when it is called by actor onDone
* Remove unused imports
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)
* Fix injectin project to the fileTree machine
* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"
This reverts commit 4b43ff69d1
.
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)
* Re-run CI
* Restore success toasts on file/folder deletion
* Replace casting with guarding against event.type
* Remove console.log
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
* Replace all instances of event casting with guards against event.type
---------
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
This commit is contained in:
committed by
GitHub
parent
7c2cfba0ac
commit
5f8d4f8294
Binary file not shown.
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@ -34,7 +34,7 @@
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@tweenjs/tween.js": "^23.1.1",
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"@xstate/react": "^4.1.1",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
@ -64,7 +64,7 @@
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"web-vitals": "^3.5.2",
|
||||
"xstate": "^4.38.2"
|
||||
"xstate": "^5.17.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -95,7 +95,7 @@ export function App() {
|
||||
})
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
if (state.matches('idle.showPlanes')) return
|
||||
if (state.matches({ idle: 'showPlanes' })) return
|
||||
if (context.store?.buttonDownInStream !== undefined) return
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
|
@ -70,12 +70,12 @@ export function Toolbar({
|
||||
*/
|
||||
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
||||
() => ({
|
||||
modelingStateMatches: state.matches,
|
||||
modelingState: state,
|
||||
modelingSend: send,
|
||||
commandBarSend,
|
||||
sketchPathId,
|
||||
}),
|
||||
[state.matches, send, commandBarSend, sketchPathId]
|
||||
[state, send, commandBarSend, sketchPathId]
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -124,9 +124,9 @@ export const ClientSideScene = ({
|
||||
} else if (context.mouseState.type === 'isDragging') {
|
||||
cursor = 'grabbing'
|
||||
} else if (
|
||||
state.matches('Sketch.Line tool') ||
|
||||
state.matches('Sketch.Tangential arc to') ||
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
state.matches({ Sketch: 'Line tool' }) ||
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||
state.matches({ Sketch: 'Rectangle tool' })
|
||||
) {
|
||||
cursor = 'crosshair'
|
||||
} else {
|
||||
@ -214,9 +214,9 @@ const Overlay = ({
|
||||
overlay.visible &&
|
||||
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
|
||||
!(
|
||||
state.matches('Sketch.Line tool') ||
|
||||
state.matches('Sketch.Tangential arc to') ||
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
state.matches({ Sketch: 'Line tool' }) ||
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||
state.matches({ Sketch: 'Rectangle tool' })
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -1,53 +1,43 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { createActorContext } from '@xstate/react'
|
||||
import { editorManager } from 'lib/singletons'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { createContext, useEffect } from 'react'
|
||||
import { EventFrom, StateFrom } from 'xstate'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type CommandsContextType = {
|
||||
commandBarState: StateFrom<typeof commandBarMachine>
|
||||
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,
|
||||
export const CommandsContext = createActorContext(
|
||||
commandBarMachine.provide({
|
||||
guards: {
|
||||
'Command has no arguments': (context, _event) => {
|
||||
'Command has no arguments': ({ context }) => {
|
||||
return (
|
||||
!context.selectedCommand?.args ||
|
||||
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(
|
||||
(argConfig) => argConfig.skip
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
editorManager.setCommandBarSend(commandBarSend)
|
||||
})
|
||||
|
||||
export const CommandBarProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<CommandsContext.Provider
|
||||
value={{
|
||||
commandBarState,
|
||||
commandBarSend,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CommandsContext.Provider>
|
||||
<CommandBarProviderInner>{children}</CommandBarProviderInner>
|
||||
</CommandsContext.Provider>
|
||||
)
|
||||
}
|
||||
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
|
||||
const commandBarActor = CommandsContext.useActorRef()
|
||||
|
||||
useEffect(() => {
|
||||
editorManager.setCommandBarSend(commandBarActor.send)
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||
e.preventDefault()
|
||||
commandBarSend({
|
||||
type: 'Submit command',
|
||||
data: argumentsToSubmit,
|
||||
output: argumentsToSubmit,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { StateFrom } from 'xstate'
|
||||
|
||||
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
|
||||
@ -48,15 +48,15 @@ function CommandBarSelectionInput({
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||
const initSelectionsByType = useCallback(() => {
|
||||
const selectionsByType = useMemo(() => {
|
||||
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
||||
return !selectionRangeEnd || selectionRangeEnd === code.length
|
||||
? 'none'
|
||||
: getSelectionType(selection)
|
||||
}, [selection, code])
|
||||
const selectionsByType = initSelectionsByType()
|
||||
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
||||
canSubmitSelectionArg(selectionsByType, arg)
|
||||
const canSubmitSelection = useMemo<boolean>(
|
||||
() => canSubmitSelectionArg(selectionsByType, arg),
|
||||
[selectionsByType]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -66,26 +66,18 @@ function CommandBarSelectionInput({
|
||||
// Fast-forward through this arg if it's marked as skippable
|
||||
// and we have a valid selection already
|
||||
useEffect(() => {
|
||||
console.log('selection input effect', {
|
||||
selectionsByType,
|
||||
canSubmitSelection,
|
||||
arg,
|
||||
})
|
||||
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
||||
handleSubmit({
|
||||
preventDefault: () => {},
|
||||
} as React.FormEvent<HTMLFormElement>)
|
||||
handleSubmit()
|
||||
}
|
||||
}, [selectionsByType, arg])
|
||||
}, [canSubmitSelection])
|
||||
|
||||
function handleChange() {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||
e?.preventDefault()
|
||||
|
||||
if (!canSubmitSelection) {
|
||||
setHasSubmitted(true)
|
||||
|
@ -5,13 +5,12 @@ import { PATHS } from 'lib/paths'
|
||||
import React, { createContext } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
Actor,
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
EventFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
assign,
|
||||
fromPromise,
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
@ -27,7 +26,7 @@ import { getNextDirName, getNextFileName } from 'lib/desktopFS'
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
|
||||
export const FileContext = createContext(
|
||||
@ -43,70 +42,68 @@ export const FileMachineProvider = ({
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
|
||||
const [state, send] = useMachine(fileMachine, {
|
||||
context: {
|
||||
project,
|
||||
selectedDirectory: project,
|
||||
},
|
||||
const [state, send] = useMachine(
|
||||
fileMachine.provide({
|
||||
actions: {
|
||||
navigateToFile: (context, event) => {
|
||||
if (event.data && 'name' in event.data) {
|
||||
renameToastSuccess: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
createToastSuccess: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
toastSuccess: ({ event }) => {
|
||||
if (
|
||||
event.type !== 'xstate.done.actor.rename-file' &&
|
||||
event.type !== 'xstate.done.actor.delete-file'
|
||||
)
|
||||
return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
toastError: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||
toast.error(event.output.message)
|
||||
},
|
||||
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.data.name
|
||||
event.output.name
|
||||
)}`
|
||||
)
|
||||
} else if (
|
||||
event.data &&
|
||||
'path' in event.data &&
|
||||
event.data.path.endsWith(FILE_EXT)
|
||||
event.output &&
|
||||
'path' in event.output &&
|
||||
event.output.path.endsWith(FILE_EXT)
|
||||
) {
|
||||
// Don't navigate to newly created directories
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.output.path)}`)
|
||||
}
|
||||
},
|
||||
addFileToRenamingQueue: assign({
|
||||
itemsBeingRenamed: (context, event) => [
|
||||
...context.itemsBeingRenamed,
|
||||
event.data.path,
|
||||
],
|
||||
}),
|
||||
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
|
||||
: []
|
||||
actors: {
|
||||
readFiles: fromPromise(async ({ input }) => {
|
||||
const newFiles =
|
||||
(isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
|
||||
[]
|
||||
return {
|
||||
...context.project,
|
||||
...input,
|
||||
children: newFiles,
|
||||
}
|
||||
},
|
||||
createAndOpenFile: async (context, event) => {
|
||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
||||
}),
|
||||
createAndOpenFile: fromPromise(async ({ input }) => {
|
||||
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
|
||||
if (event.data.makeDir) {
|
||||
if (input.makeDir) {
|
||||
let { name, path } = getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
@ -114,26 +111,26 @@ export const FileMachineProvider = ({
|
||||
} else {
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
||||
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Successfully created "${createdName}"`,
|
||||
path: createdPath,
|
||||
}
|
||||
},
|
||||
createFile: async (context, event) => {
|
||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
||||
}),
|
||||
createFile: fromPromise(async ({ input }) => {
|
||||
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
|
||||
if (event.data.makeDir) {
|
||||
if (input.makeDir) {
|
||||
let { name, path } = getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
@ -141,33 +138,30 @@ export const FileMachineProvider = ({
|
||||
} else {
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
||||
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||
}
|
||||
|
||||
return {
|
||||
path: createdPath,
|
||||
}
|
||||
},
|
||||
renameFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
||||
) => {
|
||||
const { oldName, newName, isDir } = event.data
|
||||
}),
|
||||
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(
|
||||
context.selectedDirectory.path,
|
||||
input.selectedDirectory.path,
|
||||
oldName
|
||||
)
|
||||
const newPath = window.electron.path.join(
|
||||
context.selectedDirectory.path,
|
||||
input.selectedDirectory.path,
|
||||
name
|
||||
)
|
||||
|
||||
@ -213,22 +207,19 @@ export const FileMachineProvider = ({
|
||||
newPath,
|
||||
oldPath,
|
||||
}
|
||||
},
|
||||
deleteFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Delete file'>
|
||||
) => {
|
||||
const isDir = !!event.data.children
|
||||
}),
|
||||
deleteFile: fromPromise(async ({ input }) => {
|
||||
const isDir = !!input.children
|
||||
|
||||
if (isDir) {
|
||||
await window.electron
|
||||
.rm(event.data.path, {
|
||||
.rm(input.path, {
|
||||
recursive: true,
|
||||
})
|
||||
.catch((e) => console.error('Error deleting directory', e))
|
||||
} else {
|
||||
await window.electron
|
||||
.rm(event.data.path)
|
||||
.rm(input.path)
|
||||
.catch((e) => console.error('Error deleting file', e))
|
||||
}
|
||||
|
||||
@ -250,32 +241,35 @@ export const FileMachineProvider = ({
|
||||
// the same path on the navigate, which doesn't cause anything to
|
||||
// refresh, leaving a stale execution state.
|
||||
navigate(0)
|
||||
return
|
||||
return {
|
||||
message: 'No more files in project, created main.kcl',
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) &&
|
||||
(input.path === file?.path || file?.path.includes(input.path)) &&
|
||||
project?.path
|
||||
) {
|
||||
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
||||
}
|
||||
|
||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
event.data.name
|
||||
}"`
|
||||
return {
|
||||
message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
input.name
|
||||
}"`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{
|
||||
input: {
|
||||
project,
|
||||
selectedDirectory: project,
|
||||
},
|
||||
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 (
|
||||
<FileContext.Provider
|
||||
|
@ -243,13 +243,13 @@ const FileTreeItem = ({
|
||||
onClickCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
onFocusCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||
@ -296,13 +296,13 @@ const FileTreeItem = ({
|
||||
onClickCapture={(e) => {
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}}
|
||||
onFocusCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
>
|
||||
@ -482,7 +482,7 @@ export const FileTreeInner = ({
|
||||
onClickCapture={(e) => {
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileContext.project,
|
||||
directory: fileContext.project,
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import React, { createContext, useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
Actor,
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
assign,
|
||||
fromPromise,
|
||||
} from 'xstate'
|
||||
import {
|
||||
SetSelections,
|
||||
getPersistedContext,
|
||||
modelingMachine,
|
||||
modelingMachineDefaultContext,
|
||||
@ -88,7 +88,7 @@ import { useFileContext } from 'hooks/useFileContext'
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
|
||||
export const ModelingMachineContext = createContext(
|
||||
@ -134,15 +134,7 @@ export const ModelingMachineProvider = ({
|
||||
// )
|
||||
|
||||
const [modelingState, modelingSend, modelingActor] = useMachine(
|
||||
modelingMachine,
|
||||
{
|
||||
context: {
|
||||
...modelingMachineDefaultContext,
|
||||
store: {
|
||||
...modelingMachineDefaultContext.store,
|
||||
...persistedContext,
|
||||
},
|
||||
},
|
||||
modelingMachine.provide({
|
||||
actions: {
|
||||
'disable copilot': () => {
|
||||
editorManager.setCopilotEnabled(false)
|
||||
@ -150,7 +142,7 @@ export const ModelingMachineProvider = ({
|
||||
'enable copilot': () => {
|
||||
editorManager.setCopilotEnabled(true)
|
||||
},
|
||||
'sketch exit execute': ({ store }) => {
|
||||
'sketch exit execute': ({ context: { store } }) => {
|
||||
;(async () => {
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
|
||||
@ -169,9 +161,9 @@ export const ModelingMachineProvider = ({
|
||||
})
|
||||
})()
|
||||
},
|
||||
'Set mouse state': assign({
|
||||
mouseState: (_, event) => event.data,
|
||||
segmentHoverMap: ({ mouseState, segmentHoverMap }, event) => {
|
||||
'Set mouse state': assign(({ context, event }) => {
|
||||
if (event.type !== 'Set mouse state') return {}
|
||||
const nextSegmentHoverMap = () => {
|
||||
if (event.data.type === 'isHovering') {
|
||||
const parent = getParentGroup(event.data.on, [
|
||||
STRAIGHT_SEGMENT,
|
||||
@ -179,23 +171,25 @@ export const ModelingMachineProvider = ({
|
||||
])
|
||||
const pathToNode = parent?.userData?.pathToNode
|
||||
const pathToNodeString = JSON.stringify(pathToNode)
|
||||
if (!parent || !pathToNode) return segmentHoverMap
|
||||
if (segmentHoverMap[pathToNodeString] !== undefined)
|
||||
clearTimeout(segmentHoverMap[JSON.stringify(pathToNode)])
|
||||
if (!parent || !pathToNode) return context.segmentHoverMap
|
||||
if (context.segmentHoverMap[pathToNodeString] !== undefined)
|
||||
clearTimeout(
|
||||
context.segmentHoverMap[JSON.stringify(pathToNode)]
|
||||
)
|
||||
return {
|
||||
...segmentHoverMap,
|
||||
...context.segmentHoverMap,
|
||||
[pathToNodeString]: 0,
|
||||
}
|
||||
} else if (
|
||||
event.data.type === 'idle' &&
|
||||
mouseState.type === 'isHovering'
|
||||
context.mouseState.type === 'isHovering'
|
||||
) {
|
||||
const mouseOnParent = getParentGroup(mouseState.on, [
|
||||
const mouseOnParent = getParentGroup(context.mouseState.on, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
])
|
||||
if (!mouseOnParent || !mouseOnParent?.userData?.pathToNode)
|
||||
return segmentHoverMap
|
||||
return context.segmentHoverMap
|
||||
const pathToNodeString = JSON.stringify(
|
||||
mouseOnParent?.userData?.pathToNode
|
||||
)
|
||||
@ -209,46 +203,64 @@ export const ModelingMachineProvider = ({
|
||||
})
|
||||
}, 800) as unknown as number
|
||||
return {
|
||||
...segmentHoverMap,
|
||||
...context.segmentHoverMap,
|
||||
[pathToNodeString]: timeoutId,
|
||||
}
|
||||
} else if (event.data.type === 'timeoutEnd') {
|
||||
const copy = { ...segmentHoverMap }
|
||||
const copy = { ...context.segmentHoverMap }
|
||||
delete copy[event.data.pathToNodeString]
|
||||
return copy
|
||||
}
|
||||
return {}
|
||||
},
|
||||
}
|
||||
return {
|
||||
mouseState: event.data,
|
||||
segmentHoverMap: nextSegmentHoverMap(),
|
||||
}
|
||||
}),
|
||||
'Set Segment Overlays': assign({
|
||||
segmentOverlays: ({ segmentOverlays }, { data }) => {
|
||||
if (data.type === 'set-many') return data.overlays
|
||||
if (data.type === 'set-one')
|
||||
segmentOverlays: ({ context: { segmentOverlays }, event }) => {
|
||||
if (event.type !== 'Set Segment Overlays') return {}
|
||||
if (event.data.type === 'set-many') return event.data.overlays
|
||||
if (event.data.type === 'set-one')
|
||||
return {
|
||||
...segmentOverlays,
|
||||
[data.pathToNodeString]: data.seg,
|
||||
[event.data.pathToNodeString]: event.data.seg,
|
||||
}
|
||||
if (data.type === 'delete-one') {
|
||||
if (event.data.type === 'delete-one') {
|
||||
const copy = { ...segmentOverlays }
|
||||
delete copy[data.pathToNodeString]
|
||||
delete copy[event.data.pathToNodeString]
|
||||
return copy
|
||||
}
|
||||
// data.type === 'clear'
|
||||
return {}
|
||||
},
|
||||
}),
|
||||
'Set sketchDetails': assign(({ sketchDetails }, event) =>
|
||||
sketchDetails
|
||||
? {
|
||||
'Set sketchDetails': assign(({ context: { sketchDetails }, event }) => {
|
||||
if (event.type !== 'Delete segment') return {}
|
||||
if (!sketchDetails) return {}
|
||||
return {
|
||||
sketchDetails: {
|
||||
...sketchDetails,
|
||||
sketchPathToNode: event.data,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
),
|
||||
'Set selection': assign(({ selectionRanges, sketchDetails }, event) => {
|
||||
const setSelections = event.data as SetSelections // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||
}),
|
||||
'Set selection': assign(
|
||||
({ context: { selectionRanges, sketchDetails }, event }) => {
|
||||
// this was needed for ts after adding 'Set selection' action to on done modal events
|
||||
const setSelections =
|
||||
('data' in event &&
|
||||
event.data &&
|
||||
'selectionType' in event.data &&
|
||||
event.data) ||
|
||||
('output' in event &&
|
||||
event.output &&
|
||||
'selectionType' in event.output &&
|
||||
event.output) ||
|
||||
null
|
||||
if (!setSelections) return {}
|
||||
|
||||
const dispatchSelection = (selection?: EditorSelection) => {
|
||||
if (!selection) return // TODO less of hack for the below please
|
||||
if (!editorManager.editorView) return
|
||||
@ -269,12 +281,18 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
if (setSelections.selectionType === 'singleCodeCursor') {
|
||||
if (!setSelections.selection && editorManager.isShiftDown) {
|
||||
} else if (!setSelections.selection && !editorManager.isShiftDown) {
|
||||
} else if (
|
||||
!setSelections.selection &&
|
||||
!editorManager.isShiftDown
|
||||
) {
|
||||
selections = {
|
||||
codeBasedSelections: [],
|
||||
otherSelections: [],
|
||||
}
|
||||
} else if (setSelections.selection && !editorManager.isShiftDown) {
|
||||
} else if (
|
||||
setSelections.selection &&
|
||||
!editorManager.isShiftDown
|
||||
) {
|
||||
selections = {
|
||||
codeBasedSelections: [setSelections.selection],
|
||||
otherSelections: [],
|
||||
@ -358,8 +376,9 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
|
||||
return {}
|
||||
}),
|
||||
Make: async (_, event) => {
|
||||
}
|
||||
),
|
||||
Make: async ({ event }) => {
|
||||
if (event.type !== 'Make') return
|
||||
// Check if we already have an export intent.
|
||||
if (engineCommandManager.exportIntent) {
|
||||
@ -403,7 +422,7 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
)
|
||||
},
|
||||
'Engine export': async (_, event) => {
|
||||
'Engine export': async ({ event }) => {
|
||||
if (event.type !== 'Export') return
|
||||
if (engineCommandManager.exportIntent) {
|
||||
toast.error('Already exporting')
|
||||
@ -466,8 +485,9 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
)
|
||||
},
|
||||
'Submit to Text-to-CAD API': async (_, { data }) => {
|
||||
const trimmedPrompt = data.prompt.trim()
|
||||
'Submit to Text-to-CAD API': async ({ event }) => {
|
||||
if (event.type !== 'Text-to-CAD') return
|
||||
const trimmedPrompt = event.data.prompt.trim()
|
||||
if (!trimmedPrompt) return
|
||||
|
||||
void submitAndAwaitTextToKcl({
|
||||
@ -485,7 +505,7 @@ export const ModelingMachineProvider = ({
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
'has valid extrude selection': ({ selectionRanges }) => {
|
||||
'has valid extrude selection': ({ context: { selectionRanges } }) => {
|
||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||
const isPipe = isSketchPipe(selectionRanges)
|
||||
@ -505,19 +525,22 @@ export const ModelingMachineProvider = ({
|
||||
|
||||
return canExtrudeSelection(selectionRanges)
|
||||
},
|
||||
'has valid selection for deletion': ({ selectionRanges }) => {
|
||||
'has valid selection for deletion': ({
|
||||
context: { selectionRanges },
|
||||
}) => {
|
||||
if (!commandBarState.matches('Closed')) return false
|
||||
if (selectionRanges.codeBasedSelections.length <= 0) return false
|
||||
return true
|
||||
},
|
||||
'has valid fillet selection': ({ selectionRanges }) =>
|
||||
'has valid fillet selection': ({ context: { selectionRanges } }) =>
|
||||
hasValidFilletSelection({
|
||||
selectionRanges,
|
||||
ast: kclManager.ast,
|
||||
code: codeManager.code,
|
||||
}),
|
||||
'Selection is on face': ({ selectionRanges }, { data }) => {
|
||||
if (data?.forceNewSketch) return false
|
||||
'Selection is on face': ({ context: { selectionRanges }, event }) => {
|
||||
if (event.type !== 'Enter sketch') return false
|
||||
if (event.data?.forceNewSketch) return false
|
||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
||||
return false
|
||||
return !!isCursorInSketchCommandRange(
|
||||
@ -543,8 +566,9 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
},
|
||||
},
|
||||
services: {
|
||||
'AST-undo-startSketchOn': async ({ sketchDetails }) => {
|
||||
actors: {
|
||||
'AST-undo-startSketchOn': fromPromise(
|
||||
async ({ input: { sketchDetails } }) => {
|
||||
if (!sketchDetails) return
|
||||
if (kclManager.ast.body.length) {
|
||||
// this assumes no changes have been made to the sketch besides what we did when entering the sketch
|
||||
@ -559,14 +583,17 @@ export const ModelingMachineProvider = ({
|
||||
onClick: () => {},
|
||||
onDrag: () => {},
|
||||
})
|
||||
},
|
||||
'animate-to-face': async (_, { data }) => {
|
||||
if (data.type === 'extrudeFace') {
|
||||
return undefined
|
||||
}
|
||||
),
|
||||
'animate-to-face': fromPromise(async ({ input }) => {
|
||||
if (!input) return undefined
|
||||
if (input.type === 'extrudeFace') {
|
||||
const sketched = sketchOnExtrudedFace(
|
||||
kclManager.ast,
|
||||
data.sketchPathToNode,
|
||||
data.extrudePathToNode,
|
||||
data.cap
|
||||
input.sketchPathToNode,
|
||||
input.extrudePathToNode,
|
||||
input.cap
|
||||
)
|
||||
if (trap(sketched)) return Promise.reject(sketched)
|
||||
const { modifiedAst, pathToNode: pathToNewSketchNode } = sketched
|
||||
@ -575,42 +602,45 @@ export const ModelingMachineProvider = ({
|
||||
|
||||
await letEngineAnimateAndSyncCamAfter(
|
||||
engineCommandManager,
|
||||
data.faceId
|
||||
input.faceId
|
||||
)
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
return {
|
||||
sketchPathToNode: pathToNewSketchNode,
|
||||
zAxis: data.zAxis,
|
||||
yAxis: data.yAxis,
|
||||
origin: data.position,
|
||||
zAxis: input.zAxis,
|
||||
yAxis: input.yAxis,
|
||||
origin: input.position,
|
||||
}
|
||||
}
|
||||
const { modifiedAst, pathToNode } = startSketchOnDefault(
|
||||
kclManager.ast,
|
||||
data.plane
|
||||
input.plane
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, false)
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
|
||||
await letEngineAnimateAndSyncCamAfter(
|
||||
engineCommandManager,
|
||||
data.planeId
|
||||
input.planeId
|
||||
)
|
||||
|
||||
return {
|
||||
sketchPathToNode: pathToNode,
|
||||
zAxis: data.zAxis,
|
||||
yAxis: data.yAxis,
|
||||
zAxis: input.zAxis,
|
||||
yAxis: input.yAxis,
|
||||
origin: [0, 0, 0],
|
||||
}
|
||||
},
|
||||
'animate-to-sketch': async ({ selectionRanges }) => {
|
||||
}),
|
||||
'animate-to-sketch': fromPromise(
|
||||
async ({ input: { selectionRanges } }) => {
|
||||
const sourceRange = selectionRanges.codeBasedSelections[0].range
|
||||
const sketchPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
sourceRange
|
||||
)
|
||||
const info = await getSketchOrientationDetails(sketchPathToNode || [])
|
||||
const info = await getSketchOrientationDetails(
|
||||
sketchPathToNode || []
|
||||
)
|
||||
await letEngineAnimateAndSyncCamAfter(
|
||||
engineCommandManager,
|
||||
info?.sketchDetails?.faceId || ''
|
||||
@ -623,11 +653,10 @@ export const ModelingMachineProvider = ({
|
||||
(a) => a / sceneInfra._baseUnitMultiplier
|
||||
) as [number, number, number],
|
||||
}
|
||||
},
|
||||
'Get horizontal info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
}
|
||||
),
|
||||
'Get horizontal info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintHorzVertDistance({
|
||||
constraint: 'setHorzDistance',
|
||||
@ -640,7 +669,8 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
@ -659,11 +689,10 @@ export const ModelingMachineProvider = ({
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get vertical info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
}
|
||||
),
|
||||
'Get vertical info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintHorzVertDistance({
|
||||
constraint: 'setVertDistance',
|
||||
@ -676,7 +705,8 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
@ -695,11 +725,10 @@ export const ModelingMachineProvider = ({
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get angle info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
}
|
||||
),
|
||||
'Get angle info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const info = angleBetweenInfo({
|
||||
selectionRanges,
|
||||
})
|
||||
@ -721,7 +750,8 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
@ -740,11 +770,10 @@ export const ModelingMachineProvider = ({
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get length info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
}
|
||||
),
|
||||
'Get length info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAngleLength({ selectionRanges })
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
@ -754,7 +783,8 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
@ -773,16 +803,14 @@ export const ModelingMachineProvider = ({
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get perpendicular distance info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect(
|
||||
{
|
||||
selectionRanges,
|
||||
}
|
||||
)
|
||||
),
|
||||
'Get perpendicular distance info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintIntersect({
|
||||
selectionRanges,
|
||||
})
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
@ -790,7 +818,8 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
@ -809,11 +838,10 @@ export const ModelingMachineProvider = ({
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get ABS X info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
}
|
||||
),
|
||||
'Get ABS X info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAbsDistance({
|
||||
constraint: 'xAbs',
|
||||
@ -826,7 +854,8 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
@ -845,11 +874,10 @@ export const ModelingMachineProvider = ({
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get ABS Y info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
}
|
||||
),
|
||||
'Get ABS Y info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAbsDistance({
|
||||
constraint: 'yAbs',
|
||||
@ -862,7 +890,8 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
@ -881,15 +910,14 @@ export const ModelingMachineProvider = ({
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get convert to variable info': async (
|
||||
{ sketchDetails, selectionRanges },
|
||||
{ data }
|
||||
): Promise<SetSelections> => {
|
||||
}
|
||||
),
|
||||
'Get convert to variable info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const { variableName } = await getVarNameModal({
|
||||
valueName: data.variableName || 'var',
|
||||
valueName: data?.variableName || 'var',
|
||||
})
|
||||
let parsed = parse(recast(kclManager.ast))
|
||||
if (trap(parsed)) return Promise.reject(parsed)
|
||||
@ -899,7 +927,7 @@ export const ModelingMachineProvider = ({
|
||||
moveValueIntoNewVariablePath(
|
||||
parsed,
|
||||
kclManager.programMemory,
|
||||
data.pathToNode,
|
||||
data?.pathToNode || [],
|
||||
variableName
|
||||
)
|
||||
parsed = parse(recast(_modifiedAst))
|
||||
@ -908,7 +936,8 @@ export const ModelingMachineProvider = ({
|
||||
if (!pathToReplacedNode)
|
||||
return Promise.reject(new Error('No path to replaced node'))
|
||||
|
||||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
pathToReplacedNode || [],
|
||||
parsed,
|
||||
sketchDetails.zAxis,
|
||||
@ -927,9 +956,19 @@ export const ModelingMachineProvider = ({
|
||||
selection,
|
||||
updatedPathToNode: pathToReplacedNode,
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
}),
|
||||
{
|
||||
input: {
|
||||
...modelingMachineDefaultContext,
|
||||
store: {
|
||||
...modelingMachineDefaultContext.store,
|
||||
...persistedContext,
|
||||
},
|
||||
},
|
||||
devTools: true,
|
||||
// devTools: true,
|
||||
}
|
||||
)
|
||||
|
||||
@ -965,8 +1004,8 @@ export const ModelingMachineProvider = ({
|
||||
}, [modelingSend])
|
||||
|
||||
useEffect(() => {
|
||||
editorManager.modelingEvent = modelingState.event
|
||||
}, [modelingState.event])
|
||||
editorManager.modelingState = modelingState
|
||||
}, [modelingState])
|
||||
|
||||
useEffect(() => {
|
||||
editorManager.selectionRanges = modelingState.context.selectionRanges
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from 'lib/settings/settingsTypes'
|
||||
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
||||
import { useMemo } from 'react'
|
||||
import { Event } from 'xstate'
|
||||
import { EventFrom } from 'xstate'
|
||||
|
||||
interface SettingsFieldInputProps {
|
||||
// We don't need the fancy types here,
|
||||
@ -59,7 +59,7 @@ export function SettingsFieldInput({
|
||||
level: settingsLevel,
|
||||
value: newValue,
|
||||
},
|
||||
} as unknown as Event<WildcardSetEvent>)
|
||||
} as unknown as EventFrom<WildcardSetEvent>)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@ -103,7 +103,7 @@ export function SettingsFieldInput({
|
||||
level: settingsLevel,
|
||||
value: e.target.value,
|
||||
},
|
||||
} as unknown as Event<WildcardSetEvent>)
|
||||
} as unknown as EventFrom<WildcardSetEvent>)
|
||||
}
|
||||
>
|
||||
{options &&
|
||||
@ -137,7 +137,7 @@ export function SettingsFieldInput({
|
||||
level: settingsLevel,
|
||||
value: e.target.value,
|
||||
},
|
||||
} as unknown as Event<WildcardSetEvent>)
|
||||
} as unknown as EventFrom<WildcardSetEvent>)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -14,13 +14,7 @@ import {
|
||||
Themes,
|
||||
} from 'lib/theme'
|
||||
import decamelize from 'decamelize'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
} from 'xstate'
|
||||
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||
@ -39,7 +33,7 @@ import { saveSettings } from 'lib/settings/settingsUtils'
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
|
||||
type SettingsAuthContextType = {
|
||||
@ -50,7 +44,7 @@ type SettingsAuthContextType = {
|
||||
// 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
|
||||
// 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 SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||
@ -101,21 +95,19 @@ export const SettingsAuthProviderBase = ({
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
|
||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||
settingsMachine,
|
||||
{
|
||||
context: loadedSettings,
|
||||
settingsMachine.provide({
|
||||
actions: {
|
||||
//TODO: batch all these and if that's difficult to do from tsx,
|
||||
// make it easy to do
|
||||
|
||||
setClientSideSceneUnits: (context, event) => {
|
||||
setClientSideSceneUnits: ({ context, event }) => {
|
||||
const newBaseUnit =
|
||||
event.type === 'set.modeling.defaultUnit'
|
||||
? (event.data.value as BaseUnit)
|
||||
: context.modeling.defaultUnit.current
|
||||
sceneInfra.baseUnit = newBaseUnit
|
||||
},
|
||||
setEngineTheme: (context) => {
|
||||
setEngineTheme: ({ context }) => {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
@ -135,16 +127,16 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
})
|
||||
},
|
||||
setEngineScaleGridVisibility: (context) => {
|
||||
setEngineScaleGridVisibility: ({ context }) => {
|
||||
engineCommandManager.setScaleGridVisibility(
|
||||
context.modeling.showScaleGrid.current
|
||||
)
|
||||
},
|
||||
setClientTheme: (context) => {
|
||||
setClientTheme: ({ context }) => {
|
||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||
sceneInfra.theme = opposingTheme
|
||||
},
|
||||
setEngineEdges: (context) => {
|
||||
setEngineEdges: ({ context }) => {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
@ -154,7 +146,8 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
})
|
||||
},
|
||||
toastSuccess: (_, event) => {
|
||||
toastSuccess: ({ event }) => {
|
||||
if (!('data' in event)) return
|
||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||
keyof typeof settings,
|
||||
string
|
||||
@ -176,7 +169,7 @@ export const SettingsAuthProviderBase = ({
|
||||
id: `${event.type}.success`,
|
||||
})
|
||||
},
|
||||
'Execute AST': (context, event) => {
|
||||
'Execute AST': ({ context, event }) => {
|
||||
try {
|
||||
const allSettingsIncludesUnitChange =
|
||||
event.type === 'Set all settings' &&
|
||||
@ -204,12 +197,11 @@ export const SettingsAuthProviderBase = ({
|
||||
console.error('Error executing AST after settings change', e)
|
||||
}
|
||||
},
|
||||
},
|
||||
services: {
|
||||
'Persist settings': (context) =>
|
||||
persistSettings: ({ context }) =>
|
||||
saveSettings(context, loadedProject?.project?.path),
|
||||
},
|
||||
}
|
||||
}),
|
||||
{ input: loadedSettings }
|
||||
)
|
||||
settingsStateRef = settingsState.context
|
||||
|
||||
@ -292,7 +284,8 @@ export const SettingsAuthProviderBase = ({
|
||||
}, [settingsState.context.textEditor.blinkingCursor.current])
|
||||
|
||||
// Auth machine setup
|
||||
const [authState, authSend, authActor] = useMachine(authMachine, {
|
||||
const [authState, authSend, authActor] = useMachine(
|
||||
authMachine.provide({
|
||||
actions: {
|
||||
goToSignInPage: () => {
|
||||
navigate(PATHS.SIGN_IN)
|
||||
@ -305,6 +298,7 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'auth',
|
||||
|
@ -287,7 +287,7 @@ export const Stream = () => {
|
||||
},
|
||||
})
|
||||
if (state.matches('Sketch')) return
|
||||
if (state.matches('idle.showPlanes')) return
|
||||
if (state.matches({ idle: 'showPlanes' })) return
|
||||
|
||||
if (!context.store?.didDragInStream && btnName(e).left) {
|
||||
sendSelectEventToEngine(
|
||||
|
@ -26,7 +26,7 @@ import { sendTelemetry } from 'lib/textToCad'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { EventData, EventFrom } from 'xstate'
|
||||
import { EventFrom } from 'xstate'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
|
||||
const CANVAS_SIZE = 128
|
||||
@ -45,7 +45,7 @@ export function ToastTextToCadError({
|
||||
prompt: string
|
||||
commandBarSend: (
|
||||
event: EventFrom<typeof commandBarMachine>,
|
||||
data?: EventData
|
||||
data?: unknown
|
||||
) => void
|
||||
}) {
|
||||
return (
|
||||
@ -112,7 +112,7 @@ export function ToastTextToCadSuccess({
|
||||
token?: string
|
||||
fileMachineSend: (
|
||||
event: EventFrom<typeof fileMachine>,
|
||||
data?: EventData
|
||||
data?: unknown
|
||||
) => void
|
||||
settings: {
|
||||
theme: Themes
|
||||
|
@ -133,7 +133,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
Element: 'button',
|
||||
'data-testid': 'user-sidebar-sign-out',
|
||||
children: 'Sign out',
|
||||
onClick: () => send('Log out'),
|
||||
onClick: () => send({ type: 'Log out' }),
|
||||
className: '', // Just making TS's filter type coercion happy 😠
|
||||
},
|
||||
].filter(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
@ -11,6 +11,7 @@ import {
|
||||
forEachDiagnostic,
|
||||
setDiagnosticsEffect,
|
||||
} from '@codemirror/lint'
|
||||
import { StateFrom } from 'xstate'
|
||||
|
||||
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
||||
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
||||
@ -38,7 +39,7 @@ export default class EditorManager {
|
||||
private _lastEvent: { event: string; time: number } | null = null
|
||||
|
||||
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||
private _modelingEvent: ModelingMachineEvent | null = null
|
||||
private _modelingState: StateFrom<typeof modelingMachine> | null = null
|
||||
|
||||
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
||||
() => {}
|
||||
@ -80,8 +81,8 @@ export default class EditorManager {
|
||||
this._modelingSend = send
|
||||
}
|
||||
|
||||
set modelingEvent(event: ModelingMachineEvent) {
|
||||
this._modelingEvent = event
|
||||
set modelingState(state: StateFrom<typeof modelingMachine>) {
|
||||
this._modelingState = state
|
||||
}
|
||||
|
||||
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
||||
@ -248,13 +249,11 @@ export default class EditorManager {
|
||||
return
|
||||
}
|
||||
|
||||
const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool']
|
||||
|
||||
if (!this._modelingEvent) {
|
||||
if (!this._modelingState) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ignoreEvents.includes(this._modelingEvent.type)) {
|
||||
if (this._modelingState.matches({ Sketch: 'Change Tool' })) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useCommandsContext = () => {
|
||||
return useContext(CommandsContext)
|
||||
const commandBarActor = CommandsContext.useActorRef()
|
||||
const commandBarState = CommandsContext.useSelector((state) => state)
|
||||
return {
|
||||
commandBarSend: commandBarActor.send,
|
||||
commandBarState,
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,8 @@ export function useRefreshSettings(routeId: string = PATHS.INDEX) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
ctx.settings.send('Set all settings', {
|
||||
ctx.settings.send({
|
||||
type: 'Set all settings',
|
||||
settings: routeData,
|
||||
})
|
||||
}, [])
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react'
|
||||
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate'
|
||||
import { AnyStateMachine, Actor, StateFrom } from 'xstate'
|
||||
import { createMachineCommand } from '../lib/createMachineCommand'
|
||||
import { useCommandsContext } from './useCommandsContext'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
@ -15,6 +15,7 @@ import { useKclContext } from 'lang/KclProvider'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { useAppState } from 'AppState'
|
||||
import { getActorNextEvents } from 'lib/utils'
|
||||
|
||||
// This might not be necessary, AnyStateMachine from xstate is working
|
||||
export type AllMachines =
|
||||
@ -30,7 +31,7 @@ interface UseStateMachineCommandsArgs<
|
||||
machineId: T['id']
|
||||
state: StateFrom<T>
|
||||
send: Function
|
||||
actor: InterpreterFrom<T>
|
||||
actor: Actor<T>
|
||||
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
||||
allCommandsRequireNetwork?: boolean
|
||||
onCancel?: () => void
|
||||
@ -59,7 +60,7 @@ export default function useStateMachineCommands<
|
||||
overallState !== NetworkHealthState.Weak) ||
|
||||
isExecuting ||
|
||||
!isStreamReady
|
||||
const newCommands = state.nextEvents
|
||||
const newCommands = getActorNextEvents(state)
|
||||
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||
.flatMap((type) =>
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from 'lib/settings/settingsTypes'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { PathValue } from 'lib/types'
|
||||
import { AnyStateMachine, ContextFrom, InterpreterFrom } from 'xstate'
|
||||
import { Actor, AnyStateMachine, ContextFrom } from 'xstate'
|
||||
import { getPropertyByPath } from 'lib/objectPropertyByPath'
|
||||
import { buildCommandArgument } from 'lib/createMachineCommand'
|
||||
import decamelize from 'decamelize'
|
||||
@ -28,7 +28,7 @@ export const settingsWithCommandConfigs = (
|
||||
) as SettingsPaths[]
|
||||
|
||||
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
|
||||
actor: InterpreterFrom<T>,
|
||||
actor: Actor<T>,
|
||||
isProjectAvailable: boolean,
|
||||
hideOnLevel?: SettingsLevel
|
||||
): CommandArgument<SettingsLevel, T> => ({
|
||||
@ -55,7 +55,7 @@ interface CreateSettingsArgs {
|
||||
type: SettingsPaths
|
||||
send: Function
|
||||
context: ContextFrom<typeof settingsMachine>
|
||||
actor: InterpreterFrom<typeof settingsMachine>
|
||||
actor: Actor<typeof settingsMachine>
|
||||
isProjectAvailable: boolean
|
||||
}
|
||||
|
||||
@ -132,7 +132,7 @@ export function createSettingsCommand({
|
||||
if (data !== undefined && data !== null) {
|
||||
send({ type: `set.${type}`, data })
|
||||
} else {
|
||||
send(type)
|
||||
send({ type })
|
||||
}
|
||||
},
|
||||
args: {
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { AllMachines } from 'hooks/useStateMachineCommands'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
EventFrom,
|
||||
InterpreterFrom,
|
||||
} from 'xstate'
|
||||
import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
|
||||
import { Selection } from './selections'
|
||||
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
@ -186,7 +181,7 @@ export type CommandArgument<
|
||||
machineContext?: ContextFrom<T>
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
machineActor: InterpreterFrom<T>
|
||||
machineActor: Actor<T>
|
||||
/** For showing a summary display of the current value, such as in
|
||||
* the command bar's header
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
EventFrom,
|
||||
InterpreterFrom,
|
||||
Actor,
|
||||
StateFrom,
|
||||
} from 'xstate'
|
||||
import { isDesktop } from './isDesktop'
|
||||
@ -23,7 +23,7 @@ interface CreateMachineCommandProps<
|
||||
groupId: T['id']
|
||||
state: StateFrom<T>
|
||||
send: Function
|
||||
actor: InterpreterFrom<T>
|
||||
actor: Actor<T>
|
||||
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
||||
onCancel?: () => void
|
||||
}
|
||||
@ -90,9 +90,9 @@ export function createMachineCommand<
|
||||
needsReview: commandConfig.needsReview || false,
|
||||
onSubmit: (data?: S[typeof type]) => {
|
||||
if (data !== undefined && data !== null) {
|
||||
send(type, { data })
|
||||
send({ type, data })
|
||||
} else {
|
||||
send(type)
|
||||
send({ type })
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -124,7 +124,7 @@ function buildCommandArguments<
|
||||
>(
|
||||
state: StateFrom<T>,
|
||||
args: CommandConfig<T, CommandName, S>['args'],
|
||||
machineActor: InterpreterFrom<T>
|
||||
machineActor: Actor<T>
|
||||
): 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>,
|
||||
context: ContextFrom<T>,
|
||||
machineActor: InterpreterFrom<T>
|
||||
machineActor: Actor<T>
|
||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||
const baseCommandArgument = {
|
||||
description: arg.description,
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
import toast from 'react-hot-toast'
|
||||
import { FILE_EXT } from './constants'
|
||||
import { ContextFrom, EventData, EventFrom } from 'xstate'
|
||||
import { ContextFrom, EventFrom } from 'xstate'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { NavigateFunction } from 'react-router-dom'
|
||||
import crossPlatformFetch from './crossPlatformFetch'
|
||||
@ -63,12 +63,12 @@ interface TextToKclProps {
|
||||
trimmedPrompt: string
|
||||
fileMachineSend: (
|
||||
type: EventFrom<typeof fileMachine>,
|
||||
data?: EventData
|
||||
data?: unknown
|
||||
) => unknown
|
||||
navigate: NavigateFunction
|
||||
commandBarSend: (
|
||||
type: EventFrom<typeof commandBarMachine>,
|
||||
data?: EventData
|
||||
data?: unknown
|
||||
) => unknown
|
||||
context: ContextFrom<typeof fileMachine>
|
||||
token?: string
|
||||
|
@ -16,7 +16,7 @@ type ToolbarMode = {
|
||||
}
|
||||
|
||||
export interface ToolbarItemCallbackProps {
|
||||
modelingStateMatches: StateFrom<typeof modelingMachine>['matches']
|
||||
modelingState: StateFrom<typeof modelingMachine>
|
||||
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
|
||||
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
||||
sketchPathId: string | false
|
||||
@ -84,7 +84,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Extrude', groupId: 'modeling' },
|
||||
}),
|
||||
disabled: (state) => !state.can('Extrude'),
|
||||
disabled: (state) => !state.can({ type: 'Extrude' }),
|
||||
icon: 'extrude',
|
||||
status: 'available',
|
||||
title: 'Extrude',
|
||||
@ -155,7 +155,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
}),
|
||||
icon: 'fillet3d',
|
||||
status: DEV ? 'available' : 'kcl-only',
|
||||
disabled: (state) => !state.can('Fillet'),
|
||||
disabled: (state) => !state.can({ type: 'Fillet' }),
|
||||
title: 'Fillet',
|
||||
hotkey: 'F',
|
||||
description: 'Round the edges of a 3D solid.',
|
||||
@ -272,7 +272,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
}),
|
||||
disableHotkey: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') ||
|
||||
state.matches({ Sketch: 'SketchIdle' }) ||
|
||||
state.matches('Sketch no face')
|
||||
),
|
||||
icon: 'arrowLeft',
|
||||
@ -286,33 +286,37 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
'break',
|
||||
{
|
||||
id: 'line',
|
||||
onClick: ({ modelingStateMatches: matches, modelingSend }) =>
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !matches('Sketch.Line tool') ? 'line' : 'none',
|
||||
tool: !modelingState.matches({ Sketch: 'Line tool' })
|
||||
? 'line'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
icon: 'line',
|
||||
status: 'available',
|
||||
disabled: (state) =>
|
||||
state.matches('Sketch no face') ||
|
||||
state.matches('Sketch.Rectangle tool.Awaiting second corner'),
|
||||
state.matches({
|
||||
Sketch: { 'Rectangle tool': 'Awaiting second corner' },
|
||||
}),
|
||||
title: 'Line',
|
||||
hotkey: (state) =>
|
||||
state.matches('Sketch.Line tool') ? ['Esc', 'L'] : 'L',
|
||||
state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L',
|
||||
description: 'Start drawing straight lines',
|
||||
links: [],
|
||||
isActive: (state) => state.matches('Sketch.Line tool'),
|
||||
isActive: (state) => state.matches({ Sketch: 'Line tool' }),
|
||||
},
|
||||
[
|
||||
{
|
||||
id: 'tangential-arc',
|
||||
onClick: ({ modelingStateMatches, modelingSend }) =>
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingStateMatches('Sketch.Tangential arc to')
|
||||
tool: !modelingState.matches({ Sketch: 'Tangential arc to' })
|
||||
? 'tangentialArc'
|
||||
: 'none',
|
||||
},
|
||||
@ -321,13 +325,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
status: 'available',
|
||||
disabled: (state) =>
|
||||
!isEditingExistingSketch(state.context) &&
|
||||
!state.matches('Sketch.Tangential arc to'),
|
||||
!state.matches({ Sketch: 'Tangential arc to' }),
|
||||
title: 'Tangential Arc',
|
||||
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',
|
||||
links: [],
|
||||
isActive: (state) => state.matches('Sketch.Tangential arc to'),
|
||||
isActive: (state) => state.matches({ Sketch: 'Tangential arc to' }),
|
||||
},
|
||||
{
|
||||
id: 'three-point-arc',
|
||||
@ -388,11 +392,11 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
[
|
||||
{
|
||||
id: 'corner-rectangle',
|
||||
onClick: ({ modelingStateMatches, modelingSend }) =>
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingStateMatches('Sketch.Rectangle tool')
|
||||
tool: !modelingState.matches({ Sketch: 'Rectangle tool' })
|
||||
? 'rectangle'
|
||||
: 'none',
|
||||
},
|
||||
@ -401,13 +405,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
status: 'available',
|
||||
disabled: (state) =>
|
||||
!canRectangleTool(state.context) &&
|
||||
!state.matches('Sketch.Rectangle tool'),
|
||||
!state.matches({ Sketch: 'Rectangle tool' }),
|
||||
title: 'Corner rectangle',
|
||||
hotkey: (state) =>
|
||||
state.matches('Sketch.Rectangle tool') ? ['Esc', 'R'] : 'R',
|
||||
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
|
||||
description: 'Start drawing a rectangle',
|
||||
links: [],
|
||||
isActive: (state) => state.matches('Sketch.Rectangle tool'),
|
||||
isActive: (state) => state.matches({ Sketch: 'Rectangle tool' }),
|
||||
},
|
||||
{
|
||||
id: 'center-rectangle',
|
||||
@ -456,9 +460,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-length',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain length') &&
|
||||
state.can('Constrain length')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain length' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain length' }),
|
||||
@ -473,9 +476,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-angle',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain angle') &&
|
||||
state.can('Constrain angle')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain angle' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain angle' }),
|
||||
@ -489,9 +491,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-vertical',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Make segment vertical') &&
|
||||
state.can('Make segment vertical')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Make segment vertical' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Make segment vertical' }),
|
||||
@ -506,9 +507,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-horizontal',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Make segment horizontal') &&
|
||||
state.can('Make segment horizontal')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Make segment horizontal' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Make segment horizontal' }),
|
||||
@ -523,9 +523,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-parallel',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain parallel') &&
|
||||
state.can('Constrain parallel')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain parallel' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain parallel' }),
|
||||
@ -539,9 +538,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-equal-length',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain equal length') &&
|
||||
state.can('Constrain equal length')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain equal length' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain equal length' }),
|
||||
@ -555,9 +553,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-horizontal-distance',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain horizontal distance') &&
|
||||
state.can('Constrain horizontal distance')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain horizontal distance' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain horizontal distance' }),
|
||||
@ -571,9 +568,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-vertical-distance',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain vertical distance') &&
|
||||
state.can('Constrain vertical distance')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain vertical distance' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain vertical distance' }),
|
||||
@ -587,9 +583,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-absolute-x',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain ABS X') &&
|
||||
state.can('Constrain ABS X')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain ABS X' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain ABS X' }),
|
||||
@ -603,9 +598,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-absolute-y',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain ABS Y') &&
|
||||
state.can('Constrain ABS Y')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain ABS Y' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain ABS Y' }),
|
||||
@ -619,9 +613,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-perpendicular-distance',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain perpendicular distance') &&
|
||||
state.can('Constrain perpendicular distance')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain perpendicular distance' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain perpendicular distance' }),
|
||||
@ -636,9 +629,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-align-horizontal',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain horizontally align') &&
|
||||
state.can('Constrain horizontally align')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain horizontally align' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain horizontally align' }),
|
||||
@ -652,9 +644,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-align-vertical',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain vertically align') &&
|
||||
state.can('Constrain vertically align')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain vertically align' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain vertically align' }),
|
||||
@ -668,9 +659,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'snap-to-x',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain snap to X') &&
|
||||
state.can('Constrain snap to X')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain snap to X' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain snap to X' }),
|
||||
@ -684,9 +674,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'snap-to-y',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain snap to Y') &&
|
||||
state.can('Constrain snap to Y')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain snap to Y' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain snap to Y' }),
|
||||
@ -700,9 +689,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
id: 'constraint-remove',
|
||||
disabled: (state) =>
|
||||
!(
|
||||
state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.includes('Constrain remove constraints') &&
|
||||
state.can('Constrain remove constraints')
|
||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||
state.can({ type: 'Constrain remove constraints' })
|
||||
),
|
||||
onClick: ({ modelingSend }) =>
|
||||
modelingSend({ type: 'Constrain remove constraints' }),
|
||||
|
@ -2,6 +2,7 @@ import { SourceRange } from '../lang/wasm'
|
||||
|
||||
import { v4 } from 'uuid'
|
||||
import { isDesktop } from './isDesktop'
|
||||
import { AnyMachineSnapshot } from 'xstate'
|
||||
|
||||
export const uuidv4 = v4
|
||||
|
||||
@ -208,3 +209,7 @@ export function isReducedMotion(): boolean {
|
||||
export function XOR(bool1: boolean, bool2: boolean): boolean {
|
||||
return (bool1 || bool2) && !(bool1 && bool2)
|
||||
}
|
||||
|
||||
export function getActorNextEvents(snapshot: AnyMachineSnapshot) {
|
||||
return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createMachine, assign } from 'xstate'
|
||||
import { assign, setup, fromPromise } from 'xstate'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import withBaseURL from '../lib/withBaseURL'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
@ -55,23 +55,48 @@ const persistedToken =
|
||||
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
|
||||
''
|
||||
|
||||
export const authMachine = createMachine<UserContext, Events>(
|
||||
{
|
||||
export const authMachine = setup({
|
||||
types: {} as {
|
||||
context: UserContext
|
||||
events:
|
||||
| Events
|
||||
| {
|
||||
type: 'xstate.done.actor.check-logged-in'
|
||||
output: {
|
||||
user: Models['User_type']
|
||||
token: string
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
goToIndexPage: () => {},
|
||||
goToSignInPage: () => {},
|
||||
},
|
||||
actors: {
|
||||
getUser: fromPromise(({ input }: { input: { token?: string } }) =>
|
||||
getUser(input)
|
||||
),
|
||||
},
|
||||
}).createMachine({
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */
|
||||
id: 'Auth',
|
||||
initial: 'checkIfLoggedIn',
|
||||
context: {
|
||||
token: persistedToken,
|
||||
},
|
||||
states: {
|
||||
checkIfLoggedIn: {
|
||||
id: 'check-if-logged-in',
|
||||
invoke: {
|
||||
src: 'getUser',
|
||||
input: ({ context }) => ({ token: context.token }),
|
||||
id: 'check-logged-in',
|
||||
onDone: [
|
||||
{
|
||||
target: 'loggedIn',
|
||||
actions: assign((context, event) => ({
|
||||
user: event.data.user,
|
||||
token: event.data.token || context.token,
|
||||
actions: assign(({ context, event }) => ({
|
||||
user: event.output.user,
|
||||
token: event.output.token || context.token,
|
||||
})),
|
||||
},
|
||||
],
|
||||
@ -102,7 +127,7 @@ export const authMachine = createMachine<UserContext, Events>(
|
||||
'Log in': {
|
||||
target: 'checkIfLoggedIn',
|
||||
actions: assign({
|
||||
token: (_, event) => {
|
||||
token: ({ event }) => {
|
||||
const token = event.token || ''
|
||||
return token
|
||||
},
|
||||
@ -112,22 +137,10 @@ export const authMachine = createMachine<UserContext, Events>(
|
||||
},
|
||||
},
|
||||
schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
|
||||
predictableActionArguments: true,
|
||||
preserveActionOrder: true,
|
||||
context: {
|
||||
token: persistedToken,
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {},
|
||||
services: { getUser },
|
||||
guards: {},
|
||||
delays: {},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
async function getUser(context: UserContext) {
|
||||
const token = await getAndSyncStoredToken(context)
|
||||
async function getUser(input: { token?: string }) {
|
||||
const token = await getAndSyncStoredToken(input)
|
||||
const url = withBaseURL('/user')
|
||||
const headers: { [key: string]: string } = {
|
||||
'Content-Type': 'application/json',
|
||||
@ -156,7 +169,7 @@ async function getUser(context: UserContext) {
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.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
|
||||
|
||||
@ -193,13 +206,15 @@ function getCookie(cname: string): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getAndSyncStoredToken(context: UserContext): Promise<string> {
|
||||
async function getAndSyncStoredToken(input: {
|
||||
token?: string
|
||||
}): Promise<string> {
|
||||
// dev mode
|
||||
if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN
|
||||
|
||||
const token =
|
||||
context.token && context.token !== ''
|
||||
? context.token
|
||||
input.token && input.token !== ''
|
||||
? input.token
|
||||
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||
if (token) {
|
||||
// has just logged in, update storage
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { assign, fromPromise, setup } from 'xstate'
|
||||
import {
|
||||
Command,
|
||||
CommandArgument,
|
||||
@ -25,7 +25,7 @@ export type CommandBarMachineEvent =
|
||||
data: { command: Command; argDefaultValues?: { [x: string]: unknown } }
|
||||
}
|
||||
| { type: 'Deselect command' }
|
||||
| { type: 'Submit command'; data: { [x: string]: unknown } }
|
||||
| { type: 'Submit command'; output: { [x: string]: unknown } }
|
||||
| {
|
||||
type: 'Add argument'
|
||||
data: { argument: CommandArgumentWithName<unknown> }
|
||||
@ -48,12 +48,16 @@ export type CommandBarMachineEvent =
|
||||
}
|
||||
| { type: 'Submit argument'; data: { [x: string]: unknown } }
|
||||
| {
|
||||
type: 'done.invoke.validateArguments'
|
||||
data: { [x: string]: unknown }
|
||||
type: 'xstate.done.actor.validateSingleArgument'
|
||||
output: { [x: string]: unknown }
|
||||
}
|
||||
| {
|
||||
type: 'error.platform.validateArguments'
|
||||
data: { message: string; arg: CommandArgumentWithName<unknown> }
|
||||
type: 'xstate.done.actor.validateArguments'
|
||||
output: { [x: string]: unknown }
|
||||
}
|
||||
| {
|
||||
type: 'xstate.error.actor.validateArguments'
|
||||
error: { message: string; arg: CommandArgumentWithName<unknown> }
|
||||
}
|
||||
| {
|
||||
type: 'Find and select command'
|
||||
@ -68,11 +72,264 @@ export type CommandBarMachineEvent =
|
||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||
}
|
||||
|
||||
export const commandBarMachine = createMachine(
|
||||
{
|
||||
export const commandBarMachine = setup({
|
||||
types: {
|
||||
context: {} as CommandBarContext,
|
||||
events: {} as CommandBarMachineEvent,
|
||||
},
|
||||
actions: {
|
||||
enqueueValidArgsToSubmit: assign({
|
||||
argumentsToSubmit: ({ context, event }) => {
|
||||
if (event.type !== 'xstate.done.actor.validateSingleArgument') return {}
|
||||
const [argName, argData] = Object.entries(event.output)[0]
|
||||
const { currentArgument } = context
|
||||
if (!currentArgument) return {}
|
||||
return {
|
||||
...context.argumentsToSubmit,
|
||||
[argName]: argData,
|
||||
}
|
||||
},
|
||||
}),
|
||||
'Execute command': ({ context, event }) => {
|
||||
const { selectedCommand } = context
|
||||
if (!selectedCommand) return
|
||||
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:
|
||||
// that is, the first argument that is not already in the argumentsToSubmit
|
||||
// or that is not undefined, or that is not marked as "skippable".
|
||||
// TODO validate the type of the existing arguments
|
||||
let argIndex = 0
|
||||
|
||||
while (argIndex < Object.keys(selectedCommand.args).length) {
|
||||
const [argName, argConfig] = Object.entries(selectedCommand.args)[
|
||||
argIndex
|
||||
]
|
||||
const argIsRequired =
|
||||
typeof argConfig.required === 'function'
|
||||
? argConfig.required(context)
|
||||
: argConfig.required
|
||||
const mustNotSkipArg =
|
||||
argIsRequired &&
|
||||
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
||||
context.argumentsToSubmit[argName] === undefined ||
|
||||
(rejectedArg &&
|
||||
typeof rejectedArg === 'object' &&
|
||||
'name' in rejectedArg &&
|
||||
rejectedArg.name === argName))
|
||||
|
||||
if (
|
||||
mustNotSkipArg === true ||
|
||||
argIndex + 1 === Object.keys(selectedCommand.args).length
|
||||
) {
|
||||
// If we have reached the end of the arguments and none are skippable,
|
||||
// return the last argument.
|
||||
return {
|
||||
...selectedCommand.args[argName],
|
||||
name: argName,
|
||||
}
|
||||
}
|
||||
argIndex++
|
||||
}
|
||||
|
||||
// TODO: use an XState service to continue onto review step
|
||||
// if all arguments are skippable and contain values.
|
||||
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: ({ 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) => {
|
||||
for (const [argName, argConfig] of Object.entries(
|
||||
input.selectedCommand!.args!
|
||||
)) {
|
||||
let arg = input.argumentsToSubmit[argName]
|
||||
let argValue = typeof arg === 'function' ? arg(input) : arg
|
||||
|
||||
try {
|
||||
const isRequired =
|
||||
typeof argConfig.required === 'function'
|
||||
? argConfig.required(input)
|
||||
: argConfig.required
|
||||
|
||||
const resolvedDefaultValue =
|
||||
'defaultValue' in argConfig
|
||||
? typeof argConfig.defaultValue === 'function'
|
||||
? argConfig.defaultValue(input)
|
||||
: argConfig.defaultValue
|
||||
: undefined
|
||||
|
||||
const hasMismatchedDefaultValueType =
|
||||
isRequired &&
|
||||
resolvedDefaultValue !== undefined &&
|
||||
typeof argValue !== typeof resolvedDefaultValue &&
|
||||
!(argConfig.inputType === 'kcl' || argConfig.skip)
|
||||
const hasInvalidKclValue =
|
||||
argConfig.inputType === 'kcl' &&
|
||||
!(argValue as Partial<KclCommandValue> | undefined)?.valueAst
|
||||
const hasInvalidOptionsValue =
|
||||
isRequired &&
|
||||
'options' in argConfig &&
|
||||
!(
|
||||
typeof argConfig.options === 'function'
|
||||
? argConfig.options(
|
||||
input,
|
||||
argConfig.machineActor.getSnapshot().context
|
||||
)
|
||||
: argConfig.options
|
||||
).some((o) => o.value === argValue)
|
||||
|
||||
if (
|
||||
hasMismatchedDefaultValueType ||
|
||||
hasInvalidKclValue ||
|
||||
hasInvalidOptionsValue
|
||||
) {
|
||||
return reject({
|
||||
message: 'Argument payload is of the wrong type',
|
||||
arg: {
|
||||
...argConfig,
|
||||
name: argName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
(argConfig.inputType !== 'boolean' &&
|
||||
argConfig.inputType !== 'options'
|
||||
? !argValue
|
||||
: argValue === undefined) &&
|
||||
isRequired
|
||||
) {
|
||||
return reject({
|
||||
message: 'Argument payload is falsy but is required',
|
||||
arg: {
|
||||
...argConfig,
|
||||
name: argName,
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error validating argument', context, e)
|
||||
return reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
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 */
|
||||
predictableActionArguments: true,
|
||||
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
|
||||
context: {
|
||||
commands: [],
|
||||
selectedCommand: undefined,
|
||||
@ -82,7 +339,7 @@ export const commandBarMachine = createMachine(
|
||||
codeBasedSelections: [],
|
||||
},
|
||||
argumentsToSubmit: {},
|
||||
} as CommandBarContext,
|
||||
},
|
||||
id: 'Command Bar',
|
||||
initial: 'Closed',
|
||||
states: {
|
||||
@ -105,14 +362,14 @@ export const commandBarMachine = createMachine(
|
||||
|
||||
actions: [
|
||||
assign({
|
||||
commands: (context, event) =>
|
||||
commands: ({ context, event }) =>
|
||||
[...context.commands, ...event.data.commands].sort(
|
||||
sortCommands
|
||||
),
|
||||
}),
|
||||
],
|
||||
|
||||
internal: true,
|
||||
reenter: false,
|
||||
},
|
||||
|
||||
'Remove commands': {
|
||||
@ -120,7 +377,7 @@ export const commandBarMachine = createMachine(
|
||||
|
||||
actions: [
|
||||
assign({
|
||||
commands: (context, event) =>
|
||||
commands: ({ context, event }) =>
|
||||
context.commands.filter(
|
||||
(c) =>
|
||||
!event.data.commands.some(
|
||||
@ -130,7 +387,7 @@ export const commandBarMachine = createMachine(
|
||||
}),
|
||||
],
|
||||
|
||||
internal: true,
|
||||
reenter: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -148,12 +405,12 @@ export const commandBarMachine = createMachine(
|
||||
always: [
|
||||
{
|
||||
target: 'Closed',
|
||||
cond: 'Command has no arguments',
|
||||
guard: 'Command has no arguments',
|
||||
actions: ['Execute command'],
|
||||
},
|
||||
{
|
||||
target: 'Checking Arguments',
|
||||
cond: 'All arguments are skippable',
|
||||
guard: 'All arguments are skippable',
|
||||
},
|
||||
{
|
||||
target: 'Gathering arguments',
|
||||
@ -175,22 +432,14 @@ export const commandBarMachine = createMachine(
|
||||
Validating: {
|
||||
invoke: {
|
||||
src: 'Validate argument',
|
||||
id: 'validateArgument',
|
||||
id: 'validateSingleArgument',
|
||||
input: ({ event }) => {
|
||||
if (event.type !== 'Submit argument') return {}
|
||||
return event.data
|
||||
},
|
||||
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,
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
actions: ['enqueueValidArgsToSubmit'],
|
||||
},
|
||||
onError: [
|
||||
{
|
||||
@ -250,10 +499,11 @@ export const commandBarMachine = createMachine(
|
||||
invoke: {
|
||||
src: 'Validate all arguments',
|
||||
id: 'validateArguments',
|
||||
input: ({ context }) => context,
|
||||
onDone: [
|
||||
{
|
||||
target: 'Review',
|
||||
cond: 'Command needs review',
|
||||
guard: 'Command needs review',
|
||||
},
|
||||
{
|
||||
target: 'Closed',
|
||||
@ -276,239 +526,11 @@ export const commandBarMachine = createMachine(
|
||||
|
||||
Clear: {
|
||||
target: '#Command Bar',
|
||||
internal: true,
|
||||
reenter: false,
|
||||
actions: ['Clear argument data'],
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
events: {} as CommandBarMachineEvent,
|
||||
},
|
||||
preserveActionOrder: true,
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
'Execute command': (context, event) => {
|
||||
const { selectedCommand } = context
|
||||
if (!selectedCommand) return
|
||||
if (
|
||||
(selectedCommand?.args && event.type === 'Submit command') ||
|
||||
event.type === 'done.invoke.validateArguments'
|
||||
) {
|
||||
const resolvedArgs = {} as { [x: string]: unknown }
|
||||
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) => {
|
||||
const { selectedCommand } = context
|
||||
if (!(selectedCommand && selectedCommand.args)) return undefined
|
||||
const rejectedArg = 'data' in event && event.data.arg
|
||||
|
||||
// Find the first argument that is not to be skipped:
|
||||
// that is, the first argument that is not already in the argumentsToSubmit
|
||||
// or that is not undefined, or that is not marked as "skippable".
|
||||
// TODO validate the type of the existing arguments
|
||||
let argIndex = 0
|
||||
|
||||
while (argIndex < Object.keys(selectedCommand.args).length) {
|
||||
const [argName, argConfig] = Object.entries(selectedCommand.args)[
|
||||
argIndex
|
||||
]
|
||||
const argIsRequired =
|
||||
typeof argConfig.required === 'function'
|
||||
? argConfig.required(context)
|
||||
: argConfig.required
|
||||
const mustNotSkipArg =
|
||||
argIsRequired &&
|
||||
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
||||
context.argumentsToSubmit[argName] === undefined ||
|
||||
(rejectedArg && rejectedArg.name === argName))
|
||||
|
||||
if (
|
||||
mustNotSkipArg === true ||
|
||||
argIndex + 1 === Object.keys(selectedCommand.args).length
|
||||
) {
|
||||
// If we have reached the end of the arguments and none are skippable,
|
||||
// return the last argument.
|
||||
return {
|
||||
...selectedCommand.args[argName],
|
||||
name: argName,
|
||||
}
|
||||
}
|
||||
argIndex++
|
||||
}
|
||||
|
||||
// TODO: use an XState service to continue onto review step
|
||||
// if all arguments are skippable and contain values.
|
||||
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, _) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
for (const [argName, argConfig] of Object.entries(
|
||||
context.selectedCommand!.args!
|
||||
)) {
|
||||
let arg = context.argumentsToSubmit[argName]
|
||||
let argValue = typeof arg === 'function' ? arg(context) : arg
|
||||
|
||||
try {
|
||||
const isRequired =
|
||||
typeof argConfig.required === 'function'
|
||||
? argConfig.required(context)
|
||||
: argConfig.required
|
||||
|
||||
const resolvedDefaultValue =
|
||||
'defaultValue' in argConfig
|
||||
? typeof argConfig.defaultValue === 'function'
|
||||
? argConfig.defaultValue(context)
|
||||
: argConfig.defaultValue
|
||||
: undefined
|
||||
|
||||
const hasMismatchedDefaultValueType =
|
||||
isRequired &&
|
||||
resolvedDefaultValue !== undefined &&
|
||||
typeof argValue !== typeof resolvedDefaultValue &&
|
||||
!(argConfig.inputType === 'kcl' || argConfig.skip)
|
||||
const hasInvalidKclValue =
|
||||
argConfig.inputType === 'kcl' &&
|
||||
!(argValue as Partial<KclCommandValue> | undefined)?.valueAst
|
||||
const hasInvalidOptionsValue =
|
||||
isRequired &&
|
||||
'options' in argConfig &&
|
||||
!(
|
||||
typeof argConfig.options === 'function'
|
||||
? argConfig.options(
|
||||
context,
|
||||
argConfig.machineActor.getSnapshot().context
|
||||
)
|
||||
: argConfig.options
|
||||
).some((o) => o.value === argValue)
|
||||
|
||||
if (
|
||||
hasMismatchedDefaultValueType ||
|
||||
hasInvalidKclValue ||
|
||||
hasInvalidOptionsValue
|
||||
) {
|
||||
return reject({
|
||||
message: 'Argument payload is of the wrong type',
|
||||
arg: {
|
||||
...argConfig,
|
||||
name: argName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
(argConfig.inputType !== 'boolean' &&
|
||||
argConfig.inputType !== 'options'
|
||||
? !argValue
|
||||
: argValue === undefined) &&
|
||||
isRequired
|
||||
) {
|
||||
return reject({
|
||||
message: 'Argument payload is falsy but is required',
|
||||
arg: {
|
||||
...argConfig,
|
||||
name: argName,
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error validating argument', context, e)
|
||||
return reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
return resolve(context.argumentsToSubmit)
|
||||
})
|
||||
},
|
||||
},
|
||||
delays: {},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
function sortCommands(a: Command, b: Command) {
|
||||
if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
|
||||
|
@ -1,22 +1,170 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { assign, fromPromise, setup } from 'xstate'
|
||||
import { Project, FileEntry } from 'lib/project'
|
||||
|
||||
export const fileMachine = createMachine(
|
||||
{
|
||||
type FileMachineContext = {
|
||||
project: Project
|
||||
selectedDirectory: FileEntry
|
||||
itemsBeingRenamed: (FileEntry | string)[]
|
||||
}
|
||||
|
||||
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' }
|
||||
|
||||
export const fileMachine = setup({
|
||||
types: {} as {
|
||||
context: FileMachineContext
|
||||
events: FileMachineEvents
|
||||
input: Partial<Pick<FileMachineContext, 'project' | 'selectedDirectory'>>
|
||||
},
|
||||
actions: {
|
||||
setFiles: assign(({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.read-files') return {}
|
||||
return { project: event.output }
|
||||
}),
|
||||
setSelectedDirectory: assign(({ event }) => {
|
||||
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
|
||||
},
|
||||
'Has at least 1 file': ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.read-files') return false
|
||||
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: {
|
||||
project: {} as Project,
|
||||
selectedDirectory: {} as FileEntry,
|
||||
itemsBeingRenamed: [] as string[],
|
||||
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) => ({
|
||||
actions: assign(({ event }) => ({
|
||||
...event.data,
|
||||
})),
|
||||
target: '.Reading files',
|
||||
@ -42,7 +190,7 @@ export const fileMachine = createMachine(
|
||||
'Create file': [
|
||||
{
|
||||
target: 'Creating and opening file',
|
||||
cond: 'Is not silent',
|
||||
guard: 'Is not silent',
|
||||
},
|
||||
'Creating file',
|
||||
],
|
||||
@ -66,11 +214,40 @@ export const fileMachine = createMachine(
|
||||
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: [
|
||||
'createToastSuccess',
|
||||
{
|
||||
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',
|
||||
],
|
||||
@ -89,11 +266,29 @@ export const fileMachine = createMachine(
|
||||
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'],
|
||||
cond: 'Name has been changed',
|
||||
guard: 'Name has been changed',
|
||||
},
|
||||
'Reading files',
|
||||
],
|
||||
@ -112,6 +307,21 @@ export const fileMachine = createMachine(
|
||||
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'],
|
||||
@ -129,9 +339,10 @@ export const fileMachine = createMachine(
|
||||
invoke: {
|
||||
id: 'read-files',
|
||||
src: 'readFiles',
|
||||
input: ({ context }) => context.project,
|
||||
onDone: [
|
||||
{
|
||||
cond: 'Has at least 1 file',
|
||||
guard: 'Has at least 1 file',
|
||||
target: 'Has files',
|
||||
actions: ['setFiles'],
|
||||
},
|
||||
@ -157,77 +368,26 @@ export const fileMachine = createMachine(
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
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: {
|
||||
setFiles: assign((_, event) => {
|
||||
return { project: event.data }
|
||||
}),
|
||||
setSelectedDirectory: assign((_, event) => {
|
||||
return { selectedDirectory: event.data }
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
'Name has been changed': (_, event) => {
|
||||
return event.data.newPath !== event.data.oldPath
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -1,23 +1,78 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { assign, fromPromise, setup } from 'xstate'
|
||||
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
export const homeMachine = createMachine(
|
||||
{
|
||||
export const homeMachine = setup({
|
||||
types: {
|
||||
context: {} as {
|
||||
projects: Project[]
|
||||
defaultProjectName: string
|
||||
defaultDirectory: string
|
||||
},
|
||||
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: 'xstate.done.actor.read-projects'
|
||||
output: Project[]
|
||||
}
|
||||
| { type: 'assign'; data: { [key: string]: any } },
|
||||
input: {} as {
|
||||
projects: Project[]
|
||||
defaultProjectName: string
|
||||
defaultDirectory: string
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setProjects: assign({
|
||||
projects: ({ context, event }) =>
|
||||
'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: [] as Project[],
|
||||
projects: [],
|
||||
defaultProjectName: '',
|
||||
defaultDirectory: '',
|
||||
},
|
||||
|
||||
on: {
|
||||
assign: {
|
||||
actions: assign((_, event) => ({
|
||||
actions: assign(({ event }) => ({
|
||||
...event.data,
|
||||
})),
|
||||
target: '.Reading projects',
|
||||
@ -56,6 +111,16 @@ export const homeMachine = createMachine(
|
||||
invoke: {
|
||||
id: 'create-project',
|
||||
src: 'createProject',
|
||||
input: ({ event }) => {
|
||||
if (event.type !== 'Create project') {
|
||||
return {
|
||||
name: '',
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: event.data.name,
|
||||
}
|
||||
},
|
||||
onDone: [
|
||||
{
|
||||
target: 'Reading projects',
|
||||
@ -75,6 +140,23 @@ export const homeMachine = createMachine(
|
||||
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',
|
||||
@ -94,6 +176,19 @@ export const homeMachine = createMachine(
|
||||
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'],
|
||||
@ -113,7 +208,7 @@ export const homeMachine = createMachine(
|
||||
src: 'readProjects',
|
||||
onDone: [
|
||||
{
|
||||
cond: 'Has at least 1 project',
|
||||
guard: 'Has at least 1 project',
|
||||
target: 'Has projects',
|
||||
actions: ['setProjects'],
|
||||
},
|
||||
@ -135,30 +230,4 @@ export const homeMachine = createMachine(
|
||||
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: {
|
||||
setProjects: assign((_, event) => {
|
||||
return { projects: event.data as Project[] }
|
||||
}),
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { assign, setup } from 'xstate'
|
||||
import { Themes, getSystemTheme, setThemeClass } from 'lib/theme'
|
||||
import { createSettings, settings } from 'lib/settings/initialSettings'
|
||||
import {
|
||||
@ -9,13 +9,90 @@ import {
|
||||
WildcardSetEvent,
|
||||
} from 'lib/settings/settingsTypes'
|
||||
|
||||
export const settingsMachine = createMachine(
|
||||
{
|
||||
export const settingsMachine = setup({
|
||||
types: {
|
||||
context: {} as ReturnType<typeof createSettings>,
|
||||
input: {} as ReturnType<typeof createSettings>,
|
||||
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 },
|
||||
},
|
||||
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
|
||||
|
||||
return newSettings
|
||||
}),
|
||||
setAllSettings: assign(({ event }) => {
|
||||
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]
|
||||
|
||||
// @ts-ignore
|
||||
context[category][setting][level] = value
|
||||
|
||||
const newContext = {
|
||||
...context,
|
||||
[category]: {
|
||||
...context[category],
|
||||
// @ts-ignore
|
||||
[setting]: context[category][setting],
|
||||
},
|
||||
}
|
||||
|
||||
return newContext
|
||||
}),
|
||||
setThemeClass: ({ context }) => {
|
||||
const currentTheme = context.app.theme.current ?? Themes.System
|
||||
setThemeClass(
|
||||
currentTheme === Themes.System ? getSystemTheme() : currentTheme
|
||||
)
|
||||
},
|
||||
},
|
||||
}).createMachine({
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
|
||||
id: 'Settings',
|
||||
predictableActionArguments: true,
|
||||
context: {} as ReturnType<typeof createSettings>,
|
||||
initial: 'idle',
|
||||
context: ({ input }) => {
|
||||
return {
|
||||
...createSettings(),
|
||||
...input,
|
||||
}
|
||||
},
|
||||
states: {
|
||||
idle: {
|
||||
entry: ['setThemeClass', 'setClientSideSceneUnits'],
|
||||
@ -111,75 +188,8 @@ export const settingsMachine = createMachine(
|
||||
},
|
||||
|
||||
'persisting settings': {
|
||||
invoke: {
|
||||
src: 'Persist settings',
|
||||
id: 'persistSettings',
|
||||
onDone: 'idle',
|
||||
entry: ['persistSettings'],
|
||||
always: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
resetSettings: assign((context, { defaultDirectory }) => {
|
||||
// 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 = defaultDirectory
|
||||
|
||||
return newSettings
|
||||
}),
|
||||
setAllSettings: assign((_, event) => {
|
||||
return event.settings
|
||||
}),
|
||||
setSettingAtLevel: assign((context, event) => {
|
||||
const { level, value } = event.data
|
||||
const [category, setting] = event.type
|
||||
.replace(/^set./, '')
|
||||
.split('.') as [keyof typeof settings, string]
|
||||
|
||||
// @ts-ignore
|
||||
context[category][setting][level] = value
|
||||
|
||||
const newContext = {
|
||||
...context,
|
||||
[category]: {
|
||||
...context[category],
|
||||
// @ts-ignore
|
||||
[setting]: context[category][setting],
|
||||
},
|
||||
}
|
||||
|
||||
return newContext
|
||||
}),
|
||||
setThemeClass: (context) => {
|
||||
const currentTheme = context.app.theme.current ?? Themes.System
|
||||
setThemeClass(
|
||||
currentTheme === Themes.System ? getSystemTheme() : currentTheme
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -14,7 +14,7 @@ import { type HomeLoaderData } from 'lib/types'
|
||||
import Loading from 'components/Loading'
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { homeMachine } from '../machines/homeMachine'
|
||||
import { ContextFrom, EventFrom } from 'xstate'
|
||||
import { fromPromise } from 'xstate'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
getNextSearchParams,
|
||||
@ -68,18 +68,11 @@ const Home = () => {
|
||||
)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [state, send, actor] = useMachine(homeMachine, {
|
||||
context: {
|
||||
projects: loadedProjects,
|
||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
||||
defaultDirectory: settings.app.projectDirectory.current,
|
||||
},
|
||||
const [state, send, actor] = useMachine(
|
||||
homeMachine.provide({
|
||||
actions: {
|
||||
navigateToProject: (
|
||||
context: ContextFrom<typeof homeMachine>,
|
||||
event: EventFrom<typeof homeMachine>
|
||||
) => {
|
||||
if (event.data && 'name' in event.data) {
|
||||
navigateToProject: ({ context, event }) => {
|
||||
if ('data' in event && event.data && 'name' in event.data) {
|
||||
let projectPath =
|
||||
context.defaultDirectory +
|
||||
window.electron.path.sep +
|
||||
@ -95,19 +88,29 @@ const Home = () => {
|
||||
navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
|
||||
}
|
||||
},
|
||||
toastSuccess: (_, event) => toast.success((event.data || '') + ''),
|
||||
toastError: (_, event) => toast.error((event.data || '') + ''),
|
||||
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) ||
|
||||
''
|
||||
),
|
||||
},
|
||||
services: {
|
||||
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
|
||||
listProjects(),
|
||||
createProject: async (
|
||||
context: ContextFrom<typeof homeMachine>,
|
||||
event: EventFrom<typeof homeMachine, 'Create project'>
|
||||
) => {
|
||||
actors: {
|
||||
readProjects: fromPromise(() => listProjects()),
|
||||
createProject: fromPromise(async ({ input }) => {
|
||||
let name = (
|
||||
event.data && 'name' in event.data
|
||||
? event.data.name
|
||||
input && 'name' in input && input.name
|
||||
? input.name
|
||||
: settings.projects.defaultProjectName.current
|
||||
).trim()
|
||||
|
||||
@ -119,44 +122,48 @@ const Home = () => {
|
||||
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
|
||||
}),
|
||||
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(context.defaultDirectory, oldName),
|
||||
window.electron.path.join(defaultDirectory, oldName),
|
||||
name
|
||||
)
|
||||
return `Successfully renamed "${oldName}" to "${name}"`
|
||||
},
|
||||
deleteProject: async (
|
||||
context: ContextFrom<typeof homeMachine>,
|
||||
event: EventFrom<typeof homeMachine, 'Delete project'>
|
||||
) => {
|
||||
}),
|
||||
deleteProject: fromPromise(async ({ input }) => {
|
||||
await window.electron.rm(
|
||||
window.electron.path.join(context.defaultDirectory, event.data.name),
|
||||
window.electron.path.join(input.defaultDirectory, input.name),
|
||||
{
|
||||
recursive: true,
|
||||
}
|
||||
)
|
||||
return `Successfully deleted "${event.data.name}"`
|
||||
},
|
||||
return `Successfully deleted "${input.name}"`
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
'Has at least 1 project': (_, event: EventFrom<typeof homeMachine>) => {
|
||||
if (event.type !== 'done.invoke.read-projects') return false
|
||||
return event?.data?.length ? event.data?.length >= 1 : false
|
||||
'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 [searchParams, setSearchParams] = useSearchParams()
|
||||
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
||||
@ -197,14 +204,18 @@ const Home = () => {
|
||||
)
|
||||
|
||||
if (newProjectName !== project.name) {
|
||||
send('Rename project', {
|
||||
data: { oldName: project.name, newName: newProjectName },
|
||||
send({
|
||||
type: 'Rename project',
|
||||
data: { oldName: project.name, newName: newProjectName as string },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProject(project: Project) {
|
||||
send('Delete project', { data: { name: project.name || '' } })
|
||||
send({
|
||||
type: 'Delete project',
|
||||
data: { name: project.name || '' },
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@ -217,7 +228,9 @@ const Home = () => {
|
||||
<h1 className="text-3xl font-bold">Your Projects</h1>
|
||||
<ActionButton
|
||||
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"
|
||||
iconStart={{
|
||||
icon: 'plus',
|
||||
|
50
yarn.lock
50
yarn.lock
@ -2952,13 +2952,13 @@
|
||||
"@babel/types" "^7.21.4"
|
||||
recast "^0.23.1"
|
||||
|
||||
"@xstate/react@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8"
|
||||
integrity sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ==
|
||||
"@xstate/react@^4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa"
|
||||
integrity sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA==
|
||||
dependencies:
|
||||
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":
|
||||
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"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
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:
|
||||
"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==
|
||||
@ -8860,14 +8851,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm: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:
|
||||
"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==
|
||||
@ -9412,7 +9396,7 @@ use-latest@^1.2.1:
|
||||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
|
||||
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"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
"wrap-ansi-cjs@npm: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:
|
||||
"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==
|
||||
@ -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"
|
||||
integrity sha512-BTnCPBQ2iTKe4uCnHEe1hNx6VTbXU+5mQGybSQHOjTLiBi4Ryi+tL9T6N1tmqagvM8rfl4XRfvndogfWCWcdpw==
|
||||
|
||||
xstate@^4.33.4, xstate@^4.38.2:
|
||||
xstate@^4.33.4:
|
||||
version "4.38.3"
|
||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
|
||||
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:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
|
||||
|
Reference in New Issue
Block a user