Merge remote-tracking branch 'origin' into kurt-multi-profile-again

This commit is contained in:
Kurt Hutten Irev-Dev
2025-02-03 18:36:38 +11:00
847 changed files with 157001 additions and 72141 deletions

View File

@ -1,9 +1,11 @@
import { Popover } from '@headlessui/react'
import { ActionButtonProps } from './ActionButton'
import { CustomIcon } from './CustomIcon'
import Tooltip from './Tooltip'
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
name?: string
dropdownTooltipText?: string
splitMenuItems: {
id: string
label: string
@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
export function ActionButtonDropdown({
splitMenuItems,
className,
dropdownTooltipText = 'More tools',
children,
...props
}: ActionButtonSplitProps) {
@ -26,7 +29,14 @@ export function ActionButtonDropdown({
{({ close }) => (
<>
{children}
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
<Popover.Button
className={
'!border-transparent dark:!border-transparent ' +
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent ' +
'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' +
'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
}
>
<CustomIcon
name="caretDown"
className={
@ -37,6 +47,14 @@ export function ActionButtonDropdown({
<span className="sr-only">
{props.name ? props.name + ': ' : ''}open menu
</span>
<Tooltip
delay={0}
position="bottom"
hoverOnly
wrapperClassName="ui-open:!hidden"
>
{dropdownTooltipText}
</Tooltip>
</Popover.Button>
<Popover.Panel
as="ul"

View File

@ -2,11 +2,11 @@ import { Toolbar } from '../Toolbar'
import UserSidebarMenu from 'components/UserSidebarMenu'
import { type IndexLoaderData } from 'lib/types'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop'
import { useUser } from 'machines/appMachine'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -24,8 +24,7 @@ export const AppHeader = ({
style,
enableMenu = false,
}: AppHeaderProps) => {
const { auth } = useSettingsAuthContext()
const user = auth?.context?.user
const user = useUser()
return (
<header

View File

@ -1,11 +1,12 @@
import { useModelingContext } from 'hooks/useModelingContext'
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { getNodeFromPath } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { useEffect, useRef, useState } from 'react'
import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange } from 'lang/wasm'
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
export function AstExplorer() {
const { context } = useModelingContext()
@ -118,19 +119,19 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
e.stopPropagation()
}}
onMouseMove={(e) => {
e.stopPropagation()
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
}}
onClick={(e) => {
const range: [number, number, boolean] = [
obj?.start || 0,
obj.end || 0,
true,
]
const range = topLevelRange(obj?.start || 0, obj.end || 0)
const idInfo = codeToIdSelections([
{ codeRef: codeRefFromRange(range, kclManager.ast) },
])[0]

View File

@ -1,8 +1,8 @@
import { Combobox } from '@headlessui/react'
import { useSelector } from '@xstate/react'
import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { useEffect, useMemo, useRef, useState } from 'react'
import { AnyStateMachine, StateFrom } from 'xstate'
@ -23,7 +23,7 @@ function CommandArgOptionInput({
placeholder?: string
}) {
const actorContext = useSelector(arg.machineActor, contextSelector)
const { commandBarSend, commandBarState } = useCommandsContext()
const commandBarState = useCommandBarState()
const resolvedOptions = useMemo(
() =>
typeof arg.options === 'function'
@ -129,11 +129,13 @@ function CommandArgOptionInput({
<label
htmlFor="option-input"
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
data-testid="cmd-bar-arg-name"
>
{argName}
</label>
<Combobox.Input
id="option-input"
data-testid="cmd-bar-arg-value"
ref={inputRef}
onChange={(event) =>
!event.target.disabled && setQuery(event.target.value)
@ -141,7 +143,7 @@ function CommandArgOptionInput({
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
}

View File

@ -1,6 +1,5 @@
import { Dialog, Popover, Transition } from '@headlessui/react'
import { Fragment, useEffect } from 'react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarArgument from './CommandBarArgument'
import CommandComboBox from '../CommandComboBox'
import CommandBarReview from './CommandBarReview'
@ -8,12 +7,13 @@ import { useLocation } from 'react-router-dom'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
export const CommandBar = () => {
const { pathname } = useLocation()
const { commandBarState, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
const {
context: { selectedCommand, currentArgument, commands },
} = commandBarState
@ -22,16 +22,17 @@ export const CommandBar = () => {
// Close the command bar when navigating
useEffect(() => {
commandBarSend({ type: 'Close' })
if (commandBarState.matches('Closed')) return
commandBarActor.send({ type: 'Close' })
}, [pathname])
// Hook up keyboard shortcuts
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
if (commandBarState.context.commands.length === 0) return
if (commandBarState.matches('Closed')) {
commandBarSend({ type: 'Open' })
commandBarActor.send({ type: 'Open' })
} else {
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
}
})
@ -51,14 +52,14 @@ export const CommandBar = () => {
...entries[entries.length - 1][1],
}
commandBarSend({
commandBarActor.send({
type: 'Edit argument',
data: {
arg: currentArg,
},
})
} else {
commandBarSend({ type: 'Deselect command' })
commandBarActor.send({ type: 'Deselect command' })
}
} else {
const entries = Object.entries(selectedCommand?.args || {})
@ -67,9 +68,9 @@ export const CommandBar = () => {
)
if (index === 0) {
commandBarSend({ type: 'Deselect command' })
commandBarActor.send({ type: 'Deselect command' })
} else {
commandBarSend({
commandBarActor.send({
type: 'Change current argument',
data: {
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
@ -84,19 +85,20 @@ export const CommandBar = () => {
show={!commandBarState.matches('Closed') || false}
afterLeave={() => {
if (selectedCommand?.onCancel) selectedCommand.onCancel()
commandBarSend({ type: 'Clear' })
commandBarActor.send({ type: 'Clear' })
}}
as={Fragment}
>
<WrapperComponent
open={!commandBarState.matches('Closed') || isSelectionArgument}
onClose={() => {
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
}}
className={
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
(isSelectionArgument ? 'pointer-events-none' : '')
}
data-testid="command-bar-wrapper"
>
<Transition.Child
enter="duration-100 ease-out"
@ -121,7 +123,7 @@ export const CommandBar = () => {
)
)}
<button
onClick={() => commandBarSend({ type: 'Close' })}
onClick={() => commandBarActor.send({ type: 'Close' })}
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
>
<CustomIcon

View File

@ -2,13 +2,13 @@ import CommandArgOptionInput from './CommandArgOptionInput'
import CommandBarBasicInput from './CommandBarBasicInput'
import CommandBarSelectionInput from './CommandBarSelectionInput'
import { CommandArgument } from 'lib/commandTypes'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarHeader from './CommandBarHeader'
import CommandBarKclInput from './CommandBarKclInput'
import CommandBarTextareaInput from './CommandBarTextareaInput'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
const { commandBarState, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
const {
context: { currentArgument },
} = commandBarState
@ -16,7 +16,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
function onSubmit(data: unknown) {
if (!currentArgument) return
commandBarSend({
commandBarActor.send({
type: 'Submit argument',
data: {
[currentArgument.name]: data,

View File

@ -1,5 +1,5 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument } from 'lib/commandTypes'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { useEffect, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
@ -15,8 +15,8 @@ function CommandBarBasicInput({
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const commandBarState = useCommandBarState()
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {

View File

@ -1,4 +1,3 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from '../CustomIcon'
import React, { useState } from 'react'
import { ActionButton } from '../ActionButton'
@ -7,9 +6,10 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
import Tooltip from 'components/Tooltip'
import { roundOff } from 'lib/utils'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
const { commandBarState, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
const {
context: { selectedCommand, currentArgument, argumentsToSubmit },
} = commandBarState
@ -49,7 +49,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
]
const arg = selectedCommand?.args[argName]
if (!argName || !arg) return
commandBarSend({
commandBarActor.send({
type: 'Change current argument',
data: { arg: { ...arg, name: argName } },
})
@ -100,7 +100,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
}
disabled={!isReviewing && currentArgument?.name === argName}
onClick={() => {
commandBarSend({
commandBarActor.send({
type: isReviewing
? 'Edit argument'
: 'Change current argument',

View File

@ -7,7 +7,6 @@ import {
} from '@codemirror/autocomplete'
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
import { CustomIcon } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
import { getSystemTheme } from 'lib/theme'
@ -20,6 +19,7 @@ import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
import { useSelector } from '@xstate/react'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
const machineContextSelector = (snapshot?: {
context: Record<string, unknown>
@ -37,7 +37,7 @@ function CommandBarKclInput({
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const { commandBarSend, commandBarState } = useCommandsContext()
const commandBarState = useCommandBarState()
const previouslySetValue = commandBarState.context.argumentsToSubmit[
arg.name
] as KclCommandValue | undefined
@ -82,7 +82,7 @@ function CommandBarKclInput({
false
)
const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const editorRef = useRef<HTMLDivElement>(null)
const {

View File

@ -1,43 +0,0 @@
import { createActorContext } from '@xstate/react'
import { editorManager } from 'lib/singletons'
import { commandBarMachine } from 'machines/commandBarMachine'
import { useEffect } from 'react'
export const CommandsContext = createActorContext(
commandBarMachine.provide({
guards: {
'Command has no arguments': ({ context }) => {
return (
!context.selectedCommand?.args ||
Object.keys(context.selectedCommand?.args).length === 0
)
},
'All arguments are skippable': ({ context }) => {
return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip
)
},
},
})
)
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
return (
<CommandsContext.Provider>
<CommandBarProviderInner>{children}</CommandBarProviderInner>
</CommandsContext.Provider>
)
}
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
const commandBarActor = CommandsContext.useActorRef()
useEffect(() => {
editorManager.setCommandBarSend(commandBarActor.send)
})
return children
}

View File

@ -1,9 +1,9 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import CommandBarHeader from './CommandBarHeader'
import { useHotkeys } from 'react-hotkeys-hook'
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
const { commandBarState, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
const {
context: { argumentsToSubmit, selectedCommand },
} = commandBarState
@ -33,7 +33,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
parseInt(b.keys[0], 10) - 1
]
const arg = selectedCommand?.args[argName]
commandBarSend({
commandBarActor.send({
type: 'Edit argument',
data: { arg: { ...arg, name: argName } },
})
@ -50,7 +50,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
commandBarSend({
commandBarActor.send({
type: 'Submit command',
output: argumentsToSubmit,
})

View File

@ -1,5 +1,4 @@
import { useSelector } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Artifact } from 'lang/std/artifactGraph'
import { CommandArgument } from 'lib/commandTypes'
import {
@ -10,6 +9,7 @@ import {
import { kclManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { modelingMachine } from 'machines/modelingMachine'
import { useEffect, useMemo, useRef, useState } from 'react'
import { StateFrom } from 'xstate'
@ -17,7 +17,7 @@ import { StateFrom } from 'xstate'
const semanticEntityNames: {
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
} = {
face: ['wall', 'cap', 'solid2D'],
face: ['wall', 'cap', 'solid2d'],
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
point: [],
plane: ['defaultPlane'],
@ -49,7 +49,7 @@ function CommandBarSelectionInput({
onSubmit: (data: unknown) => void
}) {
const inputRef = useRef<HTMLInputElement>(null)
const { commandBarState, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector)
const selectionsByType = useMemo(() => {
@ -145,7 +145,7 @@ function CommandBarSelectionInput({
if (event.key === 'Backspace') {
stepBack()
} else if (event.key === 'Escape') {
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
}
}}
onChange={handleChange}

View File

@ -1,5 +1,5 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument } from 'lib/commandTypes'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { RefObject, useEffect, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
@ -15,8 +15,8 @@ function CommandBarTextareaInput({
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const commandBarState = useCommandBarState()
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const formRef = useRef<HTMLFormElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
useTextareaAutoGrow(inputRef)

View File

@ -1,16 +1,15 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import usePlatform from 'hooks/usePlatform'
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
import { commandBarActor } from 'machines/commandBarMachine'
export function CommandBarOpenButton() {
const { commandBarSend } = useCommandsContext()
const platform = usePlatform()
return (
<button
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
onClick={() => commandBarSend({ type: 'Open' })}
onClick={() => commandBarActor.send({ type: 'Open' })}
data-testid="command-bar-open-button"
>
<span>Commands</span>

View File

@ -1,9 +1,11 @@
import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
import { useEffect, useState } from 'react'
import { CustomIcon } from './CustomIcon'
import { getActorNextEvents } from 'lib/utils'
import { sortCommands } from 'lib/commandUtils'
import { commandBarActor } from 'machines/commandBarMachine'
function CommandComboBox({
options,
@ -12,14 +14,21 @@ function CommandComboBox({
options: Command[]
placeholder?: string
}) {
const { commandBarSend } = useCommandsContext()
const [query, setQuery] = useState('')
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
const defaultOption =
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
// sort disabled commands to the bottom
const sortedOptions = options
.map((command) => ({
command,
disabled: optionIsDisabled(command),
}))
.sort(sortCommands)
.map(({ command }) => command)
const fuse = new Fuse(options, {
const fuse = new Fuse(sortedOptions, {
keys: ['displayName', 'name', 'description'],
threshold: 0.3,
ignoreLocation: true,
@ -27,11 +36,11 @@ function CommandComboBox({
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : options)
setFilteredOptions(query.length > 0 ? results : sortedOptions)
}, [query])
function handleSelection(command: Command) {
commandBarSend({ type: 'Select command', data: { command } })
commandBarActor.send({ type: 'Select command', data: { command } })
}
return (
@ -42,6 +51,7 @@ function CommandComboBox({
className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit"
/>
<Combobox.Input
data-testid="cmd-bar-search"
onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
onKeyDown={(event) => {
@ -50,7 +60,7 @@ function CommandComboBox({
(event.key === 'Backspace' && !event.currentTarget.value)
) {
event.preventDefault()
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
}
}}
placeholder={
@ -65,34 +75,50 @@ function CommandComboBox({
autoFocus
/>
</div>
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.groupId + option.name + (option.displayName || '')}
value={option}
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
>
{'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" />
)}
<div className="flex-grow flex flex-col">
<p className="my-0 leading-tight">
{option.displayName || option.name}{' '}
</p>
{option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
{filteredOptions?.length ? (
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.groupId + option.name + (option.displayName || '')}
value={option}
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
disabled={optionIsDisabled(option)}
data-testid={`cmd-bar-option`}
>
{'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" />
)}
</div>
</Combobox.Option>
))}
</Combobox.Options>
<div className="flex-grow flex flex-col">
<p className="my-0 leading-tight">
{option.displayName || option.name}{' '}
</p>
{option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
)}
</div>
</Combobox.Option>
))}
</Combobox.Options>
) : (
<p className="px-4 pt-2 text-chalkboard-60 dark:text-chalkboard-50">
No results found
</p>
)}
</Combobox>
)
}
export default CommandComboBox
function optionIsDisabled(option: Command): boolean {
return (
'machineActor' in option &&
option.machineActor !== undefined &&
!getActorNextEvents(option.machineActor.getSnapshot()).includes(option.name)
)
}

View File

@ -1,24 +1,21 @@
import { useMemo } from 'react'
import { engineCommandManager } from 'lib/singletons'
import {
ArtifactGraph,
expandPlane,
PlaneArtifactRich,
} from 'lang/std/artifactGraph'
import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph'
import { ArtifactGraph } from 'lang/wasm'
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
export function DebugFeatureTree() {
const featureTree = useMemo(() => {
export function DebugArtifactGraph() {
const artifactGraphTree = useMemo(() => {
return computeTree(engineCommandManager.artifactGraph)
}, [engineCommandManager.artifactGraph])
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
return (
<details data-testid="debug-feature-tree" className="relative">
<summary>Feature Tree</summary>
{featureTree.length > 0 ? (
<summary>Artifact Graph</summary>
{artifactGraphTree.length > 0 ? (
<pre className="text-xs">
<DebugDisplayArray arr={featureTree} filterKeys={filterKeys} />
<DebugDisplayArray arr={artifactGraphTree} filterKeys={filterKeys} />
</pre>
) : (
<p>(Empty)</p>

View File

@ -12,7 +12,6 @@ import {
StateFrom,
fromPromise,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { isDesktop } from 'lib/isDesktop'
import {
@ -30,6 +29,8 @@ import {
} from 'lib/getKclSamplesManifest'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -47,9 +48,10 @@ export const FileMachineProvider = ({
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const { settings } = useSettingsAuthContext()
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const token = useToken()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
[]
)
@ -90,7 +92,7 @@ export const FileMachineProvider = ({
navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file') return
if (event.output && 'name' in event.output) {
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory +
@ -296,57 +298,65 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo(
() =>
kclCommands(
async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode({
zoomToFit: true,
})
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
makeDir: false,
},
})
}
// Either way, we want to overwrite the defaultUnit project setting
// with the sample's setting.
if (data.sampleUnits) {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: data.sampleUnits,
},
})
}
kclCommands({
authToken: token ?? '',
projectData,
settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
},
kclSamples.map((sample) => ({
value: sample.pathFromProjectDirectoryToFirstFile,
name: sample.title,
}))
).filter(
specialPropsForSampleCommand: {
onSubmit: async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode({zoomToFit: true })
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
makeDir: false,
},
})
}
// Either way, we want to overwrite the defaultUnit project setting
// with the sample's setting.
if (data.sampleUnits) {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: data.sampleUnits,
},
})
}
},
providedOptions: kclSamples.map((sample) => ({
value: sample.pathFromProjectDirectoryToFirstFile,
name: sample.title,
})),
},
}).filter(
(command) => kclSamples.length || command.name !== 'open-kcl-example'
),
[codeManager, kclManager, send, kclSamples]
)
useEffect(() => {
commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
commandBarActor.send({
type: 'Add commands',
data: { commands: kclCommandMemo },
})
return () => {
commandBarSend({
commandBarActor.send({
type: 'Remove commands',
data: { commands: kclCommandMemo },
})
}
}, [commandBarSend, kclCommandMemo])
}, [commandBarActor.send, kclCommandMemo])
return (
<FileContext.Provider

View File

@ -27,6 +27,7 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { err } from 'lib/trap'
import { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons'
import { useToken } from 'machines/appMachine'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return []
@ -69,8 +70,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { auth } = useSettingsAuthContext()
const token = auth?.context.token
const token = useToken()
const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client.

View File

@ -1,11 +1,11 @@
import { createContext, useEffect, useState } from 'react'
import { engineCommandManager } from 'lib/singletons'
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { commandBarActor } from 'machines/commandBarMachine'
export type MachinesListing = Array<
components['schemas']['MachineInfoResponse']
@ -42,8 +42,6 @@ export const MachineManagerProvider = ({
components['schemas']['MachineInfoResponse'] | null
>(null)
const commandBarActor = CommandsContext.useActorRef()
// Get the reason message for why there are no machines.
const noMachinesReason = (): string | undefined => {
if (machines.length > 0) {

View File

@ -1,10 +1,8 @@
import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon'
export const ModelStateIndicator = () => {
const [commands] = useEngineCommands()
const lastCommandType = commands[commands.length - 1]?.type
let className = 'w-6 h-6 '

View File

@ -1,4 +1,4 @@
import { useMachine } from '@xstate/react'
import { useMachine, useSelector } from '@xstate/react'
import React, {
createContext,
useEffect,
@ -11,6 +11,7 @@ import {
AnyStateMachine,
ContextFrom,
Prop,
SnapshotFrom,
StateFrom,
assign,
fromPromise,
@ -82,13 +83,13 @@ import {
isCursorInFunctionDefinition,
traverse,
} from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { err, reportRejection, trap, reject } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import {
ExportIntent,
EngineConnectionStateType,
@ -105,6 +106,8 @@ import {
} from 'lang/std/artifactGraph'
import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -116,37 +119,44 @@ export const ModelingMachineContext = createContext(
{} as MachineContext<typeof modelingMachine>
)
const commandBarIsClosedSelector = (
state: SnapshotFrom<typeof commandBarActor>
) => state.matches('Closed')
export const ModelingMachineProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const {
auth,
settings: {
context: {
app: { theme, enableSSAO },
app: { theme, enableSSAO, allowOrbitInSketchMode },
modeling: {
defaultUnit,
cameraProjection,
highlightEdges,
showScaleGrid,
cameraOrbit,
},
},
},
} = useSettingsAuthContext()
const previousAllowOrbitInSketchMode = useRef(allowOrbitInSketchMode.current)
const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData
const token = auth?.context?.token
const token = useToken()
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
const { commandBarState, commandBarSend } = useCommandsContext()
const isCommandBarClosed = useSelector(
commandBarActor,
commandBarIsClosedSelector
)
// Settings machine setup
// const retrievedSettings = useRef(
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
@ -424,7 +434,16 @@ export const ModelingMachineProvider = ({
}
if (setSelections.selectionType === 'completeSelection') {
editorManager.selectRange(setSelections.selection)
const codeMirrorSelection = editorManager.createEditorSelection(
setSelections.selection
)
kclEditorActor.send({
type: 'setLastSelectionEvent',
data: {
codeMirrorSelection,
scrollIntoView: false,
},
})
if (!sketchDetails)
return {
selectionRanges: setSelections.selection,
@ -573,7 +592,6 @@ export const ModelingMachineProvider = ({
trimmedPrompt,
fileMachineSend,
navigate,
commandBarSend,
context,
token,
settings: {
@ -587,7 +605,7 @@ export const ModelingMachineProvider = ({
'has valid selection for deletion': ({
context: { selectionRanges },
}) => {
if (!commandBarState.matches('Closed')) return false
if (!isCommandBarClosed) return false
if (selectionRanges.graphSelections.length <= 0) return false
return true
},
@ -708,7 +726,8 @@ export const ModelingMachineProvider = ({
input.plane
)
await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.enableRotate = false
sceneInfra.camControls.enableRotate =
sceneInfra.camControls._setting_allowOrbitInSketchMode
sceneInfra.camControls.syncDirection = 'clientToEngine'
await letEngineAnimateAndSyncCamAfter(
@ -723,6 +742,7 @@ export const ModelingMachineProvider = ({
zAxis: input.zAxis,
yAxis: input.yAxis,
origin: [0, 0, 0],
animateTargetId: input.planeId,
}
}),
'animate-to-sketch': fromPromise(
@ -758,6 +778,7 @@ export const ModelingMachineProvider = ({
origin: info.sketchDetails.origin.map(
(a) => a / sceneInfra._baseUnitMultiplier
) as [number, number, number],
animateTargetId: info?.sketchDetails?.faceId || '',
}
}
),
@ -1483,6 +1504,7 @@ export const ModelingMachineProvider = ({
enableSSAO: enableSSAO.current,
showScaleGrid: showScaleGrid.current,
cameraProjection: cameraProjection.current,
cameraOrbit: cameraOrbit.current,
},
token
)
@ -1512,6 +1534,13 @@ export const ModelingMachineProvider = ({
editorManager.selectionRanges = modelingState.context.selectionRanges
}, [modelingState.context.selectionRanges])
// When changing camera modes reset the camera to the default orientation to correct
// the up vector otherwise the conconical orientation for the camera modes will be
// wrong
useEffect(() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}, [cameraOrbit.current])
useEffect(() => {
const onConnectionStateChanged = ({ detail }: CustomEvent) => {
// If we are in sketch mode we need to exit it.
@ -1533,6 +1562,41 @@ export const ModelingMachineProvider = ({
}
}, [engineCommandManager.engineConnection, modelingSend])
useEffect(() => {
// Only trigger this if the state actually changes, if it stays the same do not reload the camera
if (
previousAllowOrbitInSketchMode.current === allowOrbitInSketchMode.current
) {
//no op
previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current
return
}
const inSketchMode = modelingState.matches('Sketch')
// If you are in sketch mode and you disable the orbit, return back to the normal view to the target
if (!allowOrbitInSketchMode.current) {
const targetId = modelingState.context.sketchDetails?.animateTargetId
if (inSketchMode && targetId) {
letEngineAnimateAndSyncCamAfter(engineCommandManager, targetId)
.then(() => {})
.catch((e) => {
console.error(
'failed to sync engine and client scene after disabling allow orbit in sketch mode'
)
console.error(e)
})
}
}
// While you are in sketch mode you should be able to control the enable rotate
// Once you exit it goes back to normal
if (inSketchMode) {
sceneInfra.camControls.enableRotate = allowOrbitInSketchMode.current
}
previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current
}, [allowOrbitInSketchMode])
// Allow using the delete key to delete solids
useHotkeys(['backspace', 'delete', 'del'], () => {
modelingSend({ type: 'Delete selection' })

View File

@ -1,4 +1,4 @@
import { DebugFeatureTree } from 'components/DebugFeatureTree'
import { DebugArtifactGraph } from 'components/DebugArtifactGraph'
import { AstExplorer } from '../../AstExplorer'
import { EngineCommands } from '../../EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
@ -14,7 +14,7 @@ export const DebugPane = () => {
<EngineCommands />
<CamDebugSettings />
<AstExplorer />
<DebugFeatureTree />
<DebugArtifactGraph />
</div>
</section>
</div>

View File

@ -3,6 +3,7 @@
@apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90;
@apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit;
@apply transition-colors ease-out;
@apply m-0;
}
:global(.dark) .button {

View File

@ -9,12 +9,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { commandBarActor } from 'machines/commandBarMachine'
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
const { commandBarSend } = useCommandsContext()
return (
<Menu>
@ -85,7 +84,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
<Menu.Item>
<button
onClick={() => {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'code',

View File

@ -95,9 +95,11 @@ export const processMemory = (programMemory: ProgramMemory) => {
) {
const sk = sketchFromKclValueOptional(val, key)
if (val.type === 'Solid') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
processedMemory[key] = val.value.value.map(
({ ...rest }: ExtrudeSurface) => {
return rest
}
)
} else if (!(sk instanceof Reason)) {
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest

View File

@ -15,12 +15,12 @@ import { ModelingPane } from './ModelingPane'
import { isDesktop } from 'lib/isDesktop'
import { useModelingContext } from 'hooks/useModelingContext'
import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
import { commandBarActor } from 'machines/commandBarMachine'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -37,7 +37,6 @@ function getPlatformString(): 'web' | 'desktop' {
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext()
const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus
@ -66,7 +65,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
icon: 'floppyDiskArrow',
keybinding: 'Ctrl + Shift + E',
action: () =>
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Export', groupId: 'modeling' },
}),
@ -79,7 +78,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
keybinding: 'Ctrl + Shift + M',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Make', groupId: 'modeling' },
})
@ -298,7 +297,7 @@ function ModelingPaneButton({
})
return (
<div id={paneConfig.id + '-button-holder'}>
<div id={paneConfig.id + '-button-holder'} className="relative">
<button
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick}
@ -340,7 +339,7 @@ function ModelingPaneButton({
<p
id={`${paneConfig.id}-badge`}
className={
'absolute m-0 p-0 top-1 right-0 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
}
onClick={showBadge.onClick}
title={`Click to view ${showBadge.value} notification${

View File

@ -1,7 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import {
NETWORK_HEALTH_TEXT,
NetworkHealthIndicator,
@ -12,9 +11,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context
return (
<BrowserRouter>
<CommandBarProvider>
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
</CommandBarProvider>
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
</BrowserRouter>
)
}

View File

@ -0,0 +1,68 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { OpenInDesktopAppHandler } from './OpenInDesktopAppHandler'
/**
* The behavior under test requires a router,
* so we wrap the component in a minimal router setup.
*/
function TestingMinimalRouterWrapper({
children,
location,
}: {
location?: string
children: React.ReactNode
}) {
return (
<Routes location={location}>
<Route
path="/"
element={<OpenInDesktopAppHandler>{children}</OpenInDesktopAppHandler>}
/>
</Routes>
)
}
describe('OpenInDesktopAppHandler tests', () => {
test(`does not render the modal if no query param is present`, () => {
render(
<BrowserRouter>
<TestingMinimalRouterWrapper>
<p>Dummy app contents</p>
</TestingMinimalRouterWrapper>
</BrowserRouter>
)
const dummyAppContents = screen.getByText('Dummy app contents')
const modalContents = screen.queryByText('Open in desktop app')
expect(dummyAppContents).toBeInTheDocument()
expect(modalContents).not.toBeInTheDocument()
})
test(`renders the modal if the query param is present`, () => {
render(
<BrowserRouter>
<TestingMinimalRouterWrapper location="/?ask-open-desktop">
<p>Dummy app contents</p>
</TestingMinimalRouterWrapper>
</BrowserRouter>
)
let dummyAppContents = screen.queryByText('Dummy app contents')
let modalButton = screen.queryByText('Continue to web app')
// Starts as disconnected
expect(dummyAppContents).not.toBeInTheDocument()
expect(modalButton).not.toBeFalsy()
expect(modalButton).toBeInTheDocument()
fireEvent.click(modalButton as Element)
// I don't like that you have to re-query the screen here
dummyAppContents = screen.queryByText('Dummy app contents')
modalButton = screen.queryByText('Continue to web app')
expect(dummyAppContents).toBeInTheDocument()
expect(modalButton).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,125 @@
import { getSystemTheme, Themes } from 'lib/theme'
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
import { isDesktop } from 'lib/isDesktop'
import { useSearchParams } from 'react-router-dom'
import { ASK_TO_OPEN_QUERY_PARAM } from 'lib/constants'
import { VITE_KC_SITE_BASE_URL } from 'env'
import { ActionButton } from './ActionButton'
import { Transition } from '@headlessui/react'
/**
* This component is a handler that checks if a certain query parameter
* is present, and if so, it will show a modal asking the user if they
* want to open the current page in the desktop app.
*/
export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
const theme = getSystemTheme()
const buttonClasses =
'bg-transparent flex-0 hover:bg-primary/10 dark:hover:bg-primary/10'
const pathLogomarkSvg = `${isDesktop() ? '.' : ''}/zma-logomark${
theme === Themes.Light ? '-dark' : ''
}.svg`
const [searchParams, setSearchParams] = useSearchParams()
// We also ignore this param on desktop, as it is redundant
const hasAskToOpenParam =
!isDesktop() && searchParams.has(ASK_TO_OPEN_QUERY_PARAM)
/**
* This function removes the query param to ask to open in desktop app
* and then navigates to the same route but with our custom protocol
* `zoo-studio:` instead of `https://${BASE_URL}`, to trigger the user's
* desktop app to open.
*/
function onOpenInDesktopApp() {
const newSearchParams = new URLSearchParams(globalThis.location.search)
newSearchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
const newURL = `${ZOO_STUDIO_PROTOCOL}${globalThis.location.pathname.replace(
'/',
''
)}${searchParams.size > 0 ? `?${newSearchParams.toString()}` : ''}`
globalThis.location.href = newURL
}
/**
* Just remove the query param to ask to open in desktop app
* and continue to the web app.
*/
function continueToWebApp() {
searchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
setSearchParams(searchParams)
}
return hasAskToOpenParam ? (
<Transition
appear
show={true}
as="div"
className={
theme +
` fixed inset-0 grid p-4 place-content-center ${
theme === Themes.Dark ? '!bg-chalkboard-110 text-chalkboard-20' : ''
}`
}
>
<Transition.Child
as="div"
className={`max-w-3xl py-6 px-10 flex flex-col items-center gap-8
mx-auto border rounded-lg shadow-lg dark:bg-chalkboard-100`}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
style={{ zIndex: 10 }}
>
<div>
<h1 className="text-2xl">
Launching{' '}
<img
src={pathLogomarkSvg}
className="w-48"
alt="Zoo Modeling App"
/>
</h1>
</div>
<p className="text-primary flex items-center gap-2">
Choose where to open this link...
</p>
<div className="flex flex-col md:flex-row items-start justify-between gap-4 xl:gap-8">
<div className="flex flex-col gap-2">
<ActionButton
Element="button"
className={buttonClasses + ' !text-base'}
onClick={onOpenInDesktopApp}
iconEnd={{ icon: 'arrowRight' }}
>
Open in desktop app
</ActionButton>
<ActionButton
Element="externalLink"
className={
buttonClasses +
' text-sm border-transparent justify-center dark:bg-transparent'
}
to={`${VITE_KC_SITE_BASE_URL}/modeling-app/download`}
iconEnd={{ icon: 'link', bgClassName: '!bg-transparent' }}
>
Download desktop app
</ActionButton>
</div>
<ActionButton
Element="button"
className={buttonClasses + ' -order-1 !text-base'}
onClick={continueToWebApp}
iconStart={{ icon: 'arrowLeft' }}
>
Continue to web app
</ActionButton>
</div>
</Transition.Child>
</Transition>
) : (
props.children
)
}

View File

@ -2,7 +2,7 @@ import { FormEvent, useEffect, useRef, useState } from 'react'
import { PATHS } from 'lib/paths'
import { Link } from 'react-router-dom'
import { ActionButton } from '../ActionButton'
import { FILE_EXT } from 'lib/constants'
import { FILE_EXT, PROJECT_IMAGE_NAME } from 'lib/constants'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
@ -29,7 +29,7 @@ function ProjectCard({
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
// const [imageUrl, setImageUrl] = useState('')
const [imageUrl, setImageUrl] = useState('')
let inputRef = useRef<HTMLInputElement>(null)
@ -53,18 +53,21 @@ function ProjectCard({
setNumberOfFolders(project.directory_count)
}
// async function setupImageUrl() {
// const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME)
// if (await exists(projectImagePath)) {
// const imageData = await readFile(projectImagePath)
// const blob = new Blob([imageData], { type: 'image/jpg' })
// const imageUrl = URL.createObjectURL(blob)
// setImageUrl(imageUrl)
// }
// }
async function setupImageUrl() {
const projectImagePath = window.electron.path.join(
project.path,
PROJECT_IMAGE_NAME
)
if (await window.electron.exists(projectImagePath)) {
const imageData = await window.electron.readFile(projectImagePath)
const blob = new Blob([imageData], { type: 'image/png' })
const imageUrl = URL.createObjectURL(blob)
setImageUrl(imageUrl)
}
}
void getNumberOfFiles()
// void setupImageUrl()
void setupImageUrl()
}, [project.kcl_file_count, project.directory_count])
useEffect(() => {
@ -84,7 +87,7 @@ function ProjectCard({
to={`${PATHS.FILE}/${encodeURIComponent(project.default_file)}`}
className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary"
>
{/* <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
<div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
{imageUrl && (
<img
src={imageUrl}
@ -92,7 +95,7 @@ function ProjectCard({
className="h-full w-full transition-transform group-hover:scale-105 object-cover"
/>
)}
</div> */}
</div>
<div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
{isEditing ? (
<ProjectCardRenameForm

View File

@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'lib/project'
const now = new Date()
@ -33,11 +32,9 @@ describe('ProjectSidebarMenu tests', () => {
test('Disables popover menu by default', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<SettingsAuthProviderJest>
<ProjectSidebarMenu project={projectWellFormed} />
</SettingsAuthProviderJest>
</CommandBarProvider>
<SettingsAuthProviderJest>
<ProjectSidebarMenu project={projectWellFormed} />
</SettingsAuthProviderJest>
</BrowserRouter>
)

View File

@ -7,14 +7,20 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo, useContext } from 'react'
import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import { SnapshotFrom } from 'xstate'
import { commandBarActor } from 'machines/commandBarMachine'
import { useSelector } from '@xstate/react'
import { copyFileShareLink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { DEV } from 'env'
import { useToken } from 'machines/appMachine'
const ProjectSidebarMenu = ({
project,
@ -84,6 +90,9 @@ function AppLogoLink({
)
}
const commandsSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
state.context.commands
function ProjectMenuPopover({
project,
file,
@ -95,17 +104,17 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { settings } = useSettingsAuthContext()
const token = useToken()
const machineManager = useContext(MachineManagerContext)
const commands = useSelector(commandBarActor, commandsSelector)
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
const findCommand = (obj: { name: string; groupId: string }) =>
Boolean(
commandBarState.context.commands.find(
(c) => c.name === obj.name && c.groupId === obj.groupId
)
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId)
)
const machineCount = machineManager.machines.length
@ -150,12 +159,11 @@ function ProjectMenuPopover({
),
disabled: !findCommand(exportCommandInfo),
onClick: () =>
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: exportCommandInfo,
}),
},
'break',
{
id: 'make',
Element: 'button',
@ -175,12 +183,26 @@ function ProjectMenuPopover({
),
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
onClick: () => {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: makeCommandInfo,
})
},
},
{
id: 'share-link',
Element: 'button',
children: 'Share link to file',
disabled: !DEV,
onClick: async () => {
await copyFileShareLink({
token: token ?? '',
code: codeManager.code,
name: project?.name || '',
units: settings.context.modeling.defaultUnit.current,
})
},
},
'break',
{
id: 'go-home',
@ -200,7 +222,7 @@ function ProjectMenuPopover({
[
platform,
findCommand,
commandBarSend,
commandBarActor.send,
engineCommandManager,
onProjectClose,
isDesktop,

View File

@ -1,13 +1,12 @@
import { useMachine } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { projectsMachine } from 'machines/projectsMachine'
import { createContext, useEffect, useState } from 'react'
import { createContext, useCallback, useEffect, useState } from 'react'
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
import { useLspContext } from './LspProvider'
import toast from 'react-hot-toast'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import {
createNewProjectDirectory,
@ -18,11 +17,29 @@ import {
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
getUniqueProjectName,
getNextFileName,
} from 'lib/desktopFS'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
import { isDesktop } from 'lib/isDesktop'
import { commandBarActor } from 'machines/commandBarMachine'
import {
CREATE_FILE_URL_PARAM,
FILE_EXT,
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { codeManager } from 'lib/singletons'
import {
loadAndValidateSettings,
projectConfigurationToSettingsPayload,
saveSettings,
setSettingsAtLevel,
} from 'lib/settings/settingsUtils'
import { Project } from 'lib/project'
type MachineContext<T extends AnyStateMachine> = {
state?: StateFrom<T>
@ -52,12 +69,110 @@ export const ProjectsContextProvider = ({
)
}
/**
* We need some of the functionality of the ProjectsContextProvider in the web version
* but we can't perform file system operations in the browser,
* so most of the behavior of this machine is stubbed out.
*/
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
const [searchParams, setSearchParams] = useSearchParams()
const clearImportSearchParams = useCallback(() => {
// Clear the search parameters related to the "Import file from URL" command
// or we'll never be able cancel or submit it.
searchParams.delete(CREATE_FILE_URL_PARAM)
searchParams.delete('code')
searchParams.delete('name')
searchParams.delete('units')
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const {
settings: { context: settings, send: settingsSend },
} = useSettingsAuthContext()
const [state, send, actor] = useMachine(
projectsMachine.provide({
actions: {
navigateToProject: () => {},
navigateToProjectIfNeeded: () => {},
navigateToFile: () => {},
toastSuccess: ({ event }) =>
toast.success(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
'message' in event.output &&
typeof event.output.message === 'string' &&
event.output.message) ||
''
),
toastError: ({ event }) =>
toast.error(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
typeof event.output === 'string' &&
event.output) ||
''
),
},
actors: {
readProjects: fromPromise(async () => [] as Project[]),
createProject: fromPromise(async () => ({
message: 'not implemented on web',
})),
renameProject: fromPromise(async () => ({
message: 'not implemented on web',
oldName: '',
newName: '',
})),
deleteProject: fromPromise(async () => ({
message: 'not implemented on web',
name: '',
})),
createFile: fromPromise(async ({ input }) => {
// Browser version doesn't navigate, just overwrites the current file
clearImportSearchParams()
codeManager.updateCodeStateEditor(input.code || '')
await codeManager.writeToFile()
settingsSend({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: input.units,
},
})
return {
message: 'File and units overwritten successfully',
fileName: input.name,
projectName: '',
}
}),
},
}),
{
input: {
projects: [],
defaultProjectName: settings.projects.defaultProjectName.current,
defaultDirectory: settings.app.projectDirectory.current,
},
}
)
// register all project-related command palette commands
useStateMachineCommands({
machineId: 'projects',
send,
state,
commandBarConfig: projectsCommandBarConfig,
actor,
onCancel: clearImportSearchParams,
})
return (
<ProjectsMachineContext.Provider
value={{
state: undefined,
send: () => {},
state,
send,
}}
>
{children}
@ -72,19 +187,21 @@ const ProjectsContextDesktop = ({
}) => {
const navigate = useNavigate()
const location = useLocation()
const { commandBarSend } = useCommandsContext()
const [searchParams, setSearchParams] = useSearchParams()
const clearImportSearchParams = useCallback(() => {
// Clear the search parameters related to the "Import file from URL" command
// or we'll never be able cancel or submit it.
searchParams.delete(CREATE_FILE_URL_PARAM)
searchParams.delete('code')
searchParams.delete('name')
searchParams.delete('units')
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const { onProjectOpen } = useLspContext()
const {
settings: { context: settings },
} = useSettingsAuthContext()
useEffect(() => {
console.log(
'project directory changed',
settings.app.projectDirectory.current
)
}, [settings.app.projectDirectory.current])
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectPaths, projectsDir } = useProjectsLoader([
projectsLoaderTrigger,
@ -125,7 +242,7 @@ const ProjectsContextDesktop = ({
},
null
)
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
projectPath
)}`
@ -168,6 +285,31 @@ const ProjectsContextDesktop = ({
}
}
},
navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-file') return
// For now, the browser version of create-file doesn't need to navigate
// since it just overwrites the current file.
if (!isDesktop()) return
let projectPath = window.electron.join(
context.defaultDirectory,
event.output.projectName
)
let filePath = window.electron.join(
projectPath,
event.output.fileName
)
onProjectOpen(
{
name: event.output.projectName,
path: projectPath,
},
null
)
const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent(
filePath
)}`
navigate(pathToNavigateTo)
},
toastSuccess: ({ event }) =>
toast.success(
('data' in event && typeof event.data === 'string' && event.data) ||
@ -195,16 +337,12 @@ const ProjectsContextDesktop = ({
: settings.projects.defaultProjectName.current
).trim()
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = getNextProjectIndex(name, input.projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProjectDirectory(name)
const uniqueName = getUniqueProjectName(name, input.projects)
await createNewProjectDirectory(uniqueName)
return {
message: `Successfully created "${name}"`,
name,
message: `Successfully created "${uniqueName}"`,
name: uniqueName,
}
}),
renameProject: fromPromise(async ({ input }) => {
@ -221,8 +359,6 @@ const ProjectsContextDesktop = ({
name = interpolateProjectNameWithIndex(name, nextIndex)
}
console.log('from Project')
await renameProjectDirectory(
window.electron.path.join(defaultDirectory, oldName),
name
@ -245,13 +381,82 @@ const ProjectsContextDesktop = ({
name: input.name,
}
}),
},
guards: {
'Has at least 1 project': ({ event }) => {
if (event.type !== 'xstate.done.actor.read-projects') return false
console.log(`from has at least 1 project: ${event.output.length}`)
return event.output.length ? event.output.length >= 1 : false
},
createFile: fromPromise(async ({ input }) => {
let projectName =
(input.method === 'newProject' ? input.name : input.projectName) ||
settings.projects.defaultProjectName.current
let fileName =
input.method === 'newProject'
? PROJECT_ENTRYPOINT
: input.name.endsWith(FILE_EXT)
? input.name
: input.name + FILE_EXT
let message = 'File created successfully'
const unitsConfiguration: DeepPartial<Configuration> = {
settings: {
project: {
directory: settings.app.projectDirectory.current,
},
modeling: {
base_unit: input.units,
},
},
}
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
if (needsInterpolated) {
const nextIndex = getNextProjectIndex(projectName, input.projects)
projectName = interpolateProjectNameWithIndex(
projectName,
nextIndex
)
}
// Create the project around the file if newProject
if (input.method === 'newProject') {
await createNewProjectDirectory(
projectName,
input.code,
unitsConfiguration
)
message = `Project "${projectName}" created successfully with link contents`
} else {
let projectPath = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
message = `File "${fileName}" created successfully`
const existingConfiguration = await loadAndValidateSettings(
projectPath
)
const settingsToSave = setSettingsAtLevel(
existingConfiguration.settings,
'project',
projectConfigurationToSettingsPayload(unitsConfiguration)
)
await saveSettings(settingsToSave, projectPath)
}
// Create the file
let baseDir = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
const { name, path } = getNextFileName({
entryName: fileName,
baseDir,
})
fileName = name
await window.electron.writeFile(path, input.code || '')
return {
message,
fileName,
projectName,
}
}),
},
}),
{
@ -274,6 +479,7 @@ const ProjectsContextDesktop = ({
state,
commandBarConfig: projectsCommandBarConfig,
actor,
onCancel: clearImportSearchParams,
})
return (

View File

@ -8,10 +8,10 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { useToken } from 'machines/appMachine'
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const token = useToken()
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
[]

View File

@ -2,13 +2,16 @@ import { useEffect, useState, createContext, ReactNode } from 'react'
import { useNavigation, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { markOnce } from 'lib/performance'
import { useAuthNavigation } from 'hooks/useAuthNavigation'
export const RouteProviderContext = createContext({})
export function RouteProvider({ children }: { children: ReactNode }) {
useAuthNavigation()
const [first, setFirstState] = useState(true)
const navigation = useNavigation()
const location = useLocation()
useEffect(() => {
// On initialization, the react-router-dom does not send a 'loading' state event.
// it sends an idle event first.

View File

@ -2,10 +2,7 @@ import { trap } from 'lib/trap'
import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useState } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
import {
@ -16,7 +13,6 @@ import {
} from 'lib/theme'
import decamelize from 'decamelize'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import {
kclManager,
sceneInfra,
@ -29,7 +25,6 @@ import {
createSettingsCommand,
settingsWithCommandConfigs,
} from 'lib/commandBarConfigs/settingsCommandConfig'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
import { BaseUnit } from 'lib/settings/settingsTypes'
import {
@ -42,6 +37,7 @@ import { isDesktop } from 'lib/isDesktop'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { codeManager } from 'lib/singletons'
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
import { commandBarActor } from 'machines/commandBarMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -50,7 +46,6 @@ type MachineContext<T extends AnyStateMachine> = {
}
type SettingsAuthContextType = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine>
}
@ -109,7 +104,6 @@ export const SettingsAuthProviderBase = ({
}) => {
const location = useLocation()
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const [settingsPath, setSettingsPath] = useState<string | undefined>(
undefined
)
@ -137,6 +131,11 @@ export const SettingsAuthProviderBase = ({
sceneInfra.theme = opposingTheme
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
},
setAllowOrbitInSketchMode: ({ context }) => {
sceneInfra.camControls._setting_allowOrbitInSketchMode =
context.app.allowOrbitInSketchMode.current
// ModelingMachineProvider will do a use effect to trigger the camera engine sync
},
toastSuccess: ({ event }) => {
if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [
@ -273,10 +272,10 @@ export const SettingsAuthProviderBase = ({
)
.filter((c) => c !== null) as Command[]
commandBarSend({ type: 'Add commands', data: { commands: commands } })
commandBarActor.send({ type: 'Add commands', data: { commands: commands } })
return () => {
commandBarSend({
commandBarActor.send({
type: 'Remove commands',
data: { commands },
})
@ -285,7 +284,7 @@ export const SettingsAuthProviderBase = ({
settingsState,
settingsSend,
settingsActor,
commandBarSend,
commandBarActor.send,
settingsWithCommandConfigs,
])
@ -298,7 +297,7 @@ export const SettingsAuthProviderBase = ({
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
createRouteCommands(navigate, location, filePath)
commandBarSend({
commandBarActor.send({
type: 'Remove commands',
data: {
commands: [
@ -309,12 +308,12 @@ export const SettingsAuthProviderBase = ({
},
})
if (location.pathname === PATHS.HOME) {
commandBarSend({
commandBarActor.send({
type: 'Add commands',
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
})
} else if (location.pathname.includes(PATHS.FILE)) {
commandBarSend({
commandBarActor.send({
type: 'Add commands',
data: {
commands: [
@ -366,40 +365,9 @@ export const SettingsAuthProviderBase = ({
)
}, [settingsState.context.textEditor.blinkingCursor.current])
// Auth machine setup
const [authState, authSend, authActor] = useMachine(
authMachine.provide({
actions: {
goToSignInPage: () => {
navigate(PATHS.SIGN_IN)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
logout()
},
goToIndexPage: () => {
if (location.pathname.includes(PATHS.SIGN_IN)) {
navigate(PATHS.INDEX)
}
},
},
})
)
useStateMachineCommands({
machineId: 'auth',
state: authState,
send: authSend,
commandBarConfig: authCommandBarConfig,
actor: authActor,
})
return (
<SettingsAuthContext.Provider
value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: {
state: settingsState,
context: settingsState.context,
@ -413,12 +381,3 @@ export const SettingsAuthProviderBase = ({
}
export default SettingsAuthProvider
export async function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
if (isDesktop()) return Promise.resolve(null)
return fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
}

View File

@ -17,10 +17,11 @@ import {
import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { err, reportRejection } from 'lib/trap'
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
import { ViewControlContextMenu } from './ViewControlMenu'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { useSelector } from '@xstate/react'
enum StreamState {
Playing = 'playing',
@ -35,7 +36,7 @@ export const Stream = () => {
const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext()
const { state, send } = useModelingContext()
const { commandBarState } = useCommandsContext()
const commandBarState = useCommandBarState()
const { mediaStream } = useAppStream()
const { overallState, immediateState } = useNetworkContext()
const [streamState, setStreamState] = useState(StreamState.Unset)
@ -301,7 +302,7 @@ export const Stream = () => {
return
}
const path = getArtifactOfTypes(
{ key: entity_id, types: ['path', 'solid2D', 'segment'] },
{ key: entity_id, types: ['path', 'solid2d', 'segment'] },
engineCommandManager.artifactGraph
)
if (err(path)) {

View File

@ -28,7 +28,7 @@ import { base64Decode } from 'lang/wasm'
import { sendTelemetry } from 'lib/textToCad'
import { Themes } from 'lib/theme'
import { ActionButton } from './ActionButton'
import { commandBarMachine } from 'machines/commandBarMachine'
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
import { EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine'
import { reportRejection } from 'lib/trap'
@ -43,15 +43,10 @@ export function ToastTextToCadError({
toastId,
message,
prompt,
commandBarSend,
}: {
toastId: string
message: string
prompt: string
commandBarSend: (
event: EventFrom<typeof commandBarMachine>,
data?: unknown
) => void
}) {
return (
<div className="flex flex-col justify-between gap-6">
@ -81,7 +76,7 @@ export function ToastTextToCadError({
}}
name="Edit prompt"
onClick={() => {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'modeling',

View File

@ -1,10 +1,8 @@
import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
} from '../../lang/queryAst'
import { getNodeFromPath } from '../../lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import {
transformSecondarySketchLinesTagFirst,

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections'
import { PathToNode, Program, Expr } from '../../lang/wasm'
import { PathToNode, Program, Expr, topLevelRange } from '../../lang/wasm'
import { getNodeFromPath } from '../../lang/queryAst'
import {
PathToNodeMap,
@ -41,7 +41,7 @@ export function removeConstrainingValuesInfo({
graphSelections: nodes.map(
(node): Selection => ({
codeRef: codeRefFromRange(
[node.start, node.end, true],
topLevelRange(node.start, node.end),
kclManager.ast
),
})

View File

@ -8,7 +8,6 @@ import {
} from 'react-router-dom'
import { Models } from '@kittycad/lib'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
type User = Models['User_type']
@ -124,9 +123,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
<Route
path="/file/:id"
element={
<CommandBarProvider>
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
</CommandBarProvider>
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
}
/>
),

View File

@ -4,12 +4,12 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo, useState } from 'react'
import { PATHS } from 'lib/paths'
import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform'
import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon'
import { authActor } from 'machines/appMachine'
type User = Models['User_type']
@ -20,7 +20,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate()
const send = useSettingsAuthContext()?.auth?.send
const send = authActor.send
// We filter this memoized list so that no orphan "break" elements are rendered.
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(