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:
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
16
src/components/CommandBarOverwriteWarning.tsx
Normal file
16
src/components/CommandBarOverwriteWarning.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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={{
|
||||
|
@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user