Add ability to open KCL samples in-app (#3912)

* Add a shell script to get the list of KCL samples into the app

* Add support for overwriting current file with sample

* Move these KCL commands down into FileMachineProvider

* Add support for creating a new file on desktop

* Make it so these files aren't set to "renaming mode" right away

* Add support for initializing default values that are functions

* Add E2E tests

* Add a code menu item to load a sample

* Fix tsc issues

* Remove `yarn fetch:samples` from `yarn postinstall`

* Remove change to arg initialization logic, I was holding it wrong

* Switch to use new manifest file from kcl-samples repo

* Update tests now that we use proper sample titles

* Remove double-load from units menu test

* @jtran feedback

* Don't encode `https://` that's silly

* fmt

* Update e2e/playwright/testing-samples-loading.spec.ts

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Test feedback

* Add a test step to actually check the file contents were written to (@Irev-Dev feedback)

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
Frank Noirot
2024-09-23 14:35:38 -04:00
committed by GitHub
parent 1d1bb8cee0
commit aee1d66e56
21 changed files with 693 additions and 46 deletions

View File

@ -6,8 +6,8 @@ import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
import { useEffect, useMemo, useRef, useState } from 'react'
import { AnyStateMachine, StateFrom } from 'xstate'
const contextSelector = (snapshot: StateFrom<AnyStateMachine>) =>
snapshot.context
const contextSelector = (snapshot: StateFrom<AnyStateMachine> | undefined) =>
snapshot?.context
function CommandArgOptionInput({
arg,

View File

@ -140,7 +140,11 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
JSON.stringify(argValue)
)
) : (
<em>{argValue}</em>
<em>
{arg.valueSummary
? arg.valueSummary(argValue)
: argValue}
</em>
)
) : null}
</span>

View File

@ -58,7 +58,17 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
return (
<CommandBarHeader>
<p className="px-4">Confirm {selectedCommand?.name}</p>
<p className="px-4">
{selectedCommand?.reviewMessage ? (
selectedCommand.reviewMessage instanceof Function ? (
selectedCommand.reviewMessage(commandBarState.context)
) : (
selectedCommand.reviewMessage
)
) : (
<>Confirm {selectedCommand?.name}</>
)}
</p>
<form
id="review-form"
className="absolute opacity-0 inset-0 pointer-events-none"

View File

@ -31,8 +31,8 @@ function getSemanticSelectionType(selectionType: Array<Selection['type']>) {
return Array.from(semanticSelectionType)
}
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
snapshot.context.selectionRanges
const selectionSelector = (snapshot?: StateFrom<typeof modelingMachine>) =>
snapshot?.context.selectionRanges
function CommandBarSelectionInput({
arg,
@ -49,7 +49,7 @@ function CommandBarSelectionInput({
const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector)
const selectionsByType = useMemo(() => {
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
const selectionRangeEnd = selection?.codeBasedSelections[0]?.range[1]
return !selectionRangeEnd || selectionRangeEnd === code.length
? 'none'
: getSelectionType(selection)

View File

@ -0,0 +1,16 @@
interface CommandBarOverwriteWarningProps {
heading?: string
message?: string
}
export function CommandBarOverwriteWarning({
heading = 'Overwrite current file?',
message = 'This will permanently replace the current code in the editor.',
}: CommandBarOverwriteWarningProps) {
return (
<>
<p className="font-bold text-destroy-60">{heading}</p>
<p>{message}</p>
</>
)
}

View File

@ -2,7 +2,7 @@ import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import React, { createContext } from 'react'
import React, { createContext, useEffect, useMemo } from 'react'
import { toast } from 'react-hot-toast'
import {
Actor,
@ -22,6 +22,12 @@ import {
} from 'lib/constants'
import { getProjectInfo } from 'lib/desktop'
import { getNextDirName, getNextFileName } from 'lib/desktopFS'
import { kclCommands } from 'lib/kclCommands'
import { codeManager, kclManager } from 'lib/singletons'
import {
getKclSamplesManifest,
KclSamplesManifestItem,
} from 'lib/getKclSamplesManifest'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -41,6 +47,16 @@ export const FileMachineProvider = ({
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
[]
)
useEffect(() => {
async function fetchKclSamples() {
setKclSamples(await getKclSamplesManifest())
}
fetchKclSamples().catch(reportError)
}, [])
const [state, send] = useMachine(
fileMachine.provide({
@ -121,6 +137,7 @@ export const FileMachineProvider = ({
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
shouldSetToRename: input.shouldSetToRename,
}
}),
createFile: fromPromise(async ({ input }) => {
@ -271,6 +288,46 @@ export const FileMachineProvider = ({
}
)
const kclCommandMemo = useMemo(
() =>
kclCommands(
async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true)
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
makeDir: false,
},
})
}
},
kclSamples.map((sample) => ({
value: sample.file,
name: sample.title,
}))
).filter(
(command) => kclSamples.length || command.name !== 'open-kcl-example'
),
[codeManager, kclManager, send, kclSamples]
)
useEffect(() => {
commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
return () => {
commandBarSend({
type: 'Remove commands',
data: { commands: kclCommandMemo },
})
}
}, [commandBarSend, kclCommandMemo])
return (
<FileContext.Provider
value={{

View File

@ -393,14 +393,14 @@ export const FileTreeMenu = () => {
function createFile() {
send({
type: 'Create file',
data: { name: '', makeDir: false },
data: { name: '', makeDir: false, shouldSetToRename: true },
})
}
function createFolder() {
send({
type: 'Create file',
data: { name: '', makeDir: true },
data: { name: '', makeDir: true, shouldSetToRename: true },
})
}

View File

@ -9,10 +9,12 @@ 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'
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
const { commandBarSend } = useCommandsContext()
return (
<Menu>
@ -77,6 +79,22 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
</small>
</a>
</Menu.Item>
<Menu.Item>
<button
onClick={() => {
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'code',
name: 'open-kcl-example',
},
})
}}
className={styles.button}
>
<span>Load a sample model</span>
</button>
</Menu.Item>
<Menu.Item>
<a
className={styles.button}
@ -85,7 +103,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()}
>
<span>KCL samples</span>
<span>View all samples</span>
<small>
zoo.dev
<FontAwesomeIcon