diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2fd4e6b12..4deabbc49 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -18,7 +18,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 - uses: pnpm/action-setup@v2 name: Install pnpm id: pnpm-install diff --git a/core/src/execute/debugger/format-event.ts b/core/src/execute/debugger/format-event.ts index 0d2bd5e92..787172b75 100644 --- a/core/src/execute/debugger/format-event.ts +++ b/core/src/execute/debugger/format-event.ts @@ -13,7 +13,7 @@ function eventBody(event: DebuggerEvent) { .map(([pinId, size]) => `${pinId}: ${size}`) .join(", ")}`; case DebuggerEventType.ERROR: - return `Error: ${event.val}`; + return `Error: ${JSON.stringify(event.val)}`; } } diff --git a/core/src/index.ts b/core/src/index.ts index f91eb658c..3fffd7a80 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,8 +1,9 @@ export * from "./common"; import { Pos, OMap } from "./common"; +import { FlydeFlow } from "./flow-schema"; import { - CustomNode, VisualNode, + CustomNode, InputPinsConfig, Node, NodeDefinition, @@ -38,3 +39,13 @@ export interface NodeLibraryGroup { export interface NodeLibraryData { groups: NodeLibraryGroup[]; } + +export type ImportablesResult = { + importables: Record; + errors: { path: string; message: string }[]; +}; + +export interface FlowJob { + flow: FlydeFlow; + id: string; +} diff --git a/core/src/node/macro-node.ts b/core/src/node/macro-node.ts index 7d1d7d7df..87b10b33f 100644 --- a/core/src/node/macro-node.ts +++ b/core/src/node/macro-node.ts @@ -1,56 +1,106 @@ import { CodeNode, CodeNodeDefinition, NodeMetadata } from "./node"; import type React from "react"; +import { MacroNodeInstance } from "./node-instance"; -export type MacroEditorFieldDefinitionTypeString = { - value: "string"; +export type MacroEditorFieldDefinitionType = + | "string" + | "number" + | "boolean" + | "json" + | "select" + | "longtext" + | "enum" + | "dynamic"; + +// Replace the conditional type with this mapped type +export type MacroConfigurableValueTypeMap = { + string: string; + number: number; + boolean: boolean; + json: any; + select: string | number; + dynamic: undefined; }; -export type MacroEditorFieldDefinitionTypeNumber = { - value: "number"; - min?: number; - max?: number; -}; +export type MacroConfigurableValue = { + [K in keyof MacroConfigurableValueTypeMap]: { + type: K; + value: MacroConfigurableValueTypeMap[K]; + }; +}[keyof MacroConfigurableValueTypeMap]; + +export function macroConfigurableValue( + type: MacroConfigurableValue["type"], + value: MacroConfigurableValue["value"] +): MacroConfigurableValue { + return { type, value }; +} -export type MacroEditorFieldDefinitionTypeBoolean = { - value: "boolean"; -}; +export type MacroEditorFieldDefinition = + | StringFieldDefinition + | NumberFieldDefinition + | BooleanFieldDefinition + | JsonFieldDefinition + | SelectFieldDefinition + | LongTextFieldDefinition + | EnumFieldDefinition; -export type MacroEditorFieldDefinitionTypeJson = { - value: "json"; - label?: string; -}; +interface BaseFieldDefinition { + label: string; + description?: string; + configKey: string; + templateSupport?: boolean; + typeConfigurable?: boolean; +} -export type MacroEditorFieldDefinitionTypeSelect = { - value: "select"; - items: { value: string | number; label: string }[]; -}; +export interface StringFieldDefinition extends BaseFieldDefinition { + type: "string"; +} -export type MacroEditorFieldDefinitionTypeLongText = { - value: "longtext"; - rows?: number; -}; +export interface BooleanFieldDefinition extends BaseFieldDefinition { + type: "boolean"; +} -export type MacroEditorFieldDefinitionTypeEnum = { - value: "enum"; - options: string[]; -}; +export interface JsonFieldDefinition extends BaseFieldDefinition { + type: "json"; + typeData?: { + helperText?: string; + }; +} -export type MacroEditorFieldDefinitionType = - | MacroEditorFieldDefinitionTypeString - | MacroEditorFieldDefinitionTypeNumber - | MacroEditorFieldDefinitionTypeBoolean - | MacroEditorFieldDefinitionTypeJson - | MacroEditorFieldDefinitionTypeSelect - | MacroEditorFieldDefinitionTypeLongText - | MacroEditorFieldDefinitionTypeEnum; - -export interface MacroEditorFieldDefinition { - label: string; - description?: string; - configKey: string; - allowDynamic: boolean; - type: MacroEditorFieldDefinitionType; - defaultValue?: any; +export interface LongTextFieldDefinition extends BaseFieldDefinition { + type: "longtext"; + typeData?: { + rows?: number; + }; +} + +export interface NumberFieldDefinition extends BaseFieldDefinition { + type: "number"; + typeData?: NumberTypeData; +} + +export interface SelectFieldDefinition extends BaseFieldDefinition { + type: "select"; + typeData: SelectTypeData; +} + +export interface EnumFieldDefinition extends BaseFieldDefinition { + type: "enum"; + typeData: EnumTypeData; +} + +export interface NumberTypeData { + min?: number; + max?: number; +} + +export interface SelectTypeData { + items: { value: string | number; label: string }[]; +} + +export interface EnumTypeData { + options: string[]; } export interface MacroEditorConfigCustomResolved { @@ -104,25 +154,11 @@ export type MacroNodeDefinition = Omit< export interface MacroEditorCompProps { value: T; onChange: (value: T) => void; + prompt: (message: string) => Promise; } export interface MacroEditorComp extends React.FC> {} -/* helpers, used flow-editor macro editor builder */ - -export type ConfigurableInputStatic = { - mode: "static"; - value: T; -}; - -export type ConfigurableInputDynamic = { - mode: "dynamic"; -}; - -export type ConfigurableInput = - | ConfigurableInputStatic - | ConfigurableInputDynamic; - export const isMacroNode = (p: any): p is MacroNode => { return p && typeof (p as MacroNode).runFnBuilder === "function"; }; @@ -140,3 +176,25 @@ export const isMacroNodeDefinition = ( return editorConfig?.type === "structured"; } }; + +export function processMacroNodeInstance( + namespace: string, + macro: MacroNode, + instance: MacroNodeInstance +) { + const metaData = macro.definitionBuilder(instance.macroData); + const runFn = macro.runFnBuilder(instance.macroData); + + const id = `${namespace}${macro.id}__${instance.id}`; + + const resolvedNode: CodeNode = { + ...metaData, + defaultStyle: metaData.defaultStyle ?? macro.defaultStyle, + displayName: metaData.displayName ?? macro.id, + namespace: macro.namespace, + id, + run: runFn, + }; + + return resolvedNode; +} diff --git a/dev-server/src/client.ts b/dev-server/src/client.ts index 4933687b7..c0180a1ee 100644 --- a/dev-server/src/client.ts +++ b/dev-server/src/client.ts @@ -1,16 +1,11 @@ import axios from "axios"; import { FlydeFlow, - ImportableSource, NodeLibraryData, - NodeLibraryGroup, ResolvedFlydeFlowDefinition, + ImportablesResult, } from "@flyde/core"; import { FolderStructure } from "./fs-helper/shared"; -import type { ImportablesResult } from "./service/scan-importable-nodes"; -export type { ImportablesResult } from "./service/scan-importable-nodes"; - -export * from "./runner/shared"; export const createDevServerClient = (baseUrl: string) => { return { diff --git a/dev-server/src/runner/index.ts b/dev-server/src/runner/index.ts index f329dcc93..18a1fc447 100644 --- a/dev-server/src/runner/index.ts +++ b/dev-server/src/runner/index.ts @@ -1,2 +1 @@ -export * from "./shared"; export * from "./runFlow.host"; diff --git a/dev-server/src/runner/runFlow.host.ts b/dev-server/src/runner/runFlow.host.ts index 2356ae403..63d6aab07 100644 --- a/dev-server/src/runner/runFlow.host.ts +++ b/dev-server/src/runner/runFlow.host.ts @@ -2,7 +2,7 @@ import { fork } from "child_process"; import { join } from "path"; import { runFlow } from "./runFlow"; -import { FlowJob } from "./shared"; +import type { FlowJob } from "@flyde/core"; import { onMessage, sendMessage } from "./typedProcessMessage"; export function forkRunFlow(data: { diff --git a/dev-server/src/runner/runFlow.ts b/dev-server/src/runner/runFlow.ts index 9b5b8f5da..26ad1b95d 100644 --- a/dev-server/src/runner/runFlow.ts +++ b/dev-server/src/runner/runFlow.ts @@ -1,5 +1,4 @@ -import { FlydeFlow } from "@flyde/core"; -import { FlowJob } from "./shared"; +import { FlowJob, FlydeFlow } from "@flyde/core"; import { createId } from "@paralleldrive/cuid2"; import { loadFlowFromContent } from "@flyde/runtime"; diff --git a/dev-server/src/runner/shared.ts b/dev-server/src/runner/shared.ts deleted file mode 100644 index 0e00b0980..000000000 --- a/dev-server/src/runner/shared.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FlydeFlow } from "@flyde/core"; - -export interface FlowJob { - flow: FlydeFlow; - id: string; -} \ No newline at end of file diff --git a/dev-server/src/runner/typedProcessMessage.ts b/dev-server/src/runner/typedProcessMessage.ts index abbb8e1b9..1b813a56c 100644 --- a/dev-server/src/runner/typedProcessMessage.ts +++ b/dev-server/src/runner/typedProcessMessage.ts @@ -1,6 +1,6 @@ import { ChildProcess } from "child_process"; import type { runFlow } from "./runFlow"; -import { FlowJob } from "./shared"; +import { FlowJob } from "@flyde/core"; export interface ProcessMessageEventsMap { runFlow: Parameters; diff --git a/dev-server/src/service/scan-importable-nodes.ts b/dev-server/src/service/scan-importable-nodes.ts index e6d3a808d..210c40085 100644 --- a/dev-server/src/service/scan-importable-nodes.ts +++ b/dev-server/src/service/scan-importable-nodes.ts @@ -12,6 +12,7 @@ import { isBaseNode, isMacroNode, NodesDefCollection, + ImportablesResult, } from "@flyde/core"; import { scanFolderStructure } from "./scan-folders-structure"; import { FlydeFile } from "../fs-helper/shared"; @@ -25,11 +26,6 @@ export interface CorruptScannedNode { error: string; } -export type ImportablesResult = { - importables: Record; - errors: { path: string; message: string }[]; -}; - export async function scanImportableNodes( rootPath: string, filename: string diff --git a/editor/src/integrated-flow-manager/IntegratedFlowManager.tsx b/editor/src/integrated-flow-manager/IntegratedFlowManager.tsx index db71b1cde..90bdc72a1 100644 --- a/editor/src/integrated-flow-manager/IntegratedFlowManager.tsx +++ b/editor/src/integrated-flow-manager/IntegratedFlowManager.tsx @@ -32,7 +32,7 @@ import { FlowEditor } from "@flyde/flow-editor"; // ../../common/flow-editor/Flo import { useDebouncedCallback } from "use-debounce"; import { IntegratedFlowSideMenu } from "./side-menu"; -import { NodeDefinition } from "@flyde/core"; +import { NodeDefinition, ImportablesResult } from "@flyde/core"; import { AppToaster } from "@flyde/flow-editor"; // ../../common/toaster @@ -49,7 +49,6 @@ import { useState } from "react"; import { useEffect } from "react"; import _ from "lodash"; import { useBootstrapData } from "./use-bootstrap-data"; -import type { ImportablesResult } from "@flyde/dev-server"; export const PIECE_HEIGHT = 28; diff --git a/flow-editor/package.json b/flow-editor/package.json index fa0f6d489..9a54c54c4 100644 --- a/flow-editor/package.json +++ b/flow-editor/package.json @@ -19,8 +19,8 @@ "@blueprintjs/icons": "^5.1.5", "@blueprintjs/select": "^5.0.1", "@flyde/core": "workspace:*", - "@flyde/dev-server": "workspace:*", "@flyde/remote-debugger": "workspace:*", + "@flyde/stdlib": "workspace:*", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-brands-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", diff --git a/flow-editor/src/flow-editor/DependenciesContext.tsx b/flow-editor/src/flow-editor/DependenciesContext.tsx index e8ed0b99e..11517b860 100644 --- a/flow-editor/src/flow-editor/DependenciesContext.tsx +++ b/flow-editor/src/flow-editor/DependenciesContext.tsx @@ -3,8 +3,8 @@ import { ImportableSource, Pos, NodeLibraryData, + ImportablesResult, } from "@flyde/core"; -import type { ImportablesResult } from "@flyde/dev-server"; import { createContext, useContext } from "react"; // TODO - merge this interface with the one from the dev-server diff --git a/flow-editor/src/flow-editor/ports/ports.ts b/flow-editor/src/flow-editor/ports/ports.ts index 48116364c..1e1ef7a98 100644 --- a/flow-editor/src/flow-editor/ports/ports.ts +++ b/flow-editor/src/flow-editor/ports/ports.ts @@ -5,8 +5,9 @@ import { NodeLibraryData, ResolvedDependenciesDefinitions, noop, + FlowJob, + ImportablesResult, } from "@flyde/core"; -import { FlowJob, ImportablesResult } from "@flyde/dev-server"; import { ReportEvent } from "./analytics"; import { toastMsg } from "../../toaster"; diff --git a/flow-editor/src/index.tsx b/flow-editor/src/index.tsx index 22e1275e6..1e00250b7 100644 --- a/flow-editor/src/index.tsx +++ b/flow-editor/src/index.tsx @@ -15,3 +15,4 @@ export * from "./physics"; export * from "./flow-editor/base-node-editor"; export * from "./visual-node-editor/utils"; export * from "./lib/logger"; +export * from "./visual-node-editor/MacroInstanceEditor/SimpleJsonEditor"; diff --git a/flow-editor/src/visual-node-editor/MacroInstanceEditor/ConfigurableInputEditor.tsx b/flow-editor/src/visual-node-editor/MacroInstanceEditor/ConfigurableInputEditor.tsx deleted file mode 100644 index ade747f78..000000000 --- a/flow-editor/src/visual-node-editor/MacroInstanceEditor/ConfigurableInputEditor.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Radio, RadioGroup } from "@blueprintjs/core"; -import React from "react"; -import { ConfigurableInput } from "@flyde/core"; -import { InfoTooltip } from "../../lib/InfoTooltip"; - -export interface ValueCompProps { - value: T; - onChange: (value: T) => void; -} - -export interface ConfigurableInputEditorProps { - value: ConfigurableInput; - onChange: (value: ConfigurableInput) => void; - valueRenderer: React.FC>; - modeLabel: string; - defaultStaticValue: T; -} - -export const ConfigurableInputEditor = function ({ - value, - onChange, - valueRenderer: ValueRenderer, - defaultStaticValue, - modeLabel, -}: ConfigurableInputEditorProps) { - const handleModeChange = (e: React.FormEvent) => { - onChange({ - mode: e.currentTarget.value as "static" | "dynamic", - value: - e.currentTarget.value === "static" ? defaultStaticValue : undefined, - }); - }; - - const handleValueChange = (value: T) => { - onChange({ - value, - mode: "static", - }); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - const MemoValueRenderer = React.useMemo(() => ValueRenderer, []); - return ( - <> - - {modeLabel} - - Static - can be configured with a fixed - value, using this form -
- Dynamic - a new input will be exposed so this - value can be controlled at runtime - - } - /> - - } - onChange={handleModeChange} - selectedValue={value.mode} - inline - > - - -
- {value.mode === "static" ? ( - - ) : null} - - ); -}; - -export const createConfigurableInputEditor = >( - valueRenderer: React.FC> -) => { - return (props: Omit, "valueRenderer">) => ( - - ); -}; diff --git a/flow-editor/src/visual-node-editor/MacroInstanceEditor/MacroInstanceEditor.scss b/flow-editor/src/visual-node-editor/MacroInstanceEditor/MacroInstanceEditor.scss index ec1bf715c..0bf6d36c6 100644 --- a/flow-editor/src/visual-node-editor/MacroInstanceEditor/MacroInstanceEditor.scss +++ b/flow-editor/src/visual-node-editor/MacroInstanceEditor/MacroInstanceEditor.scss @@ -14,4 +14,18 @@ margin-top: 4px; } } + + .macro-description { + margin-bottom: 10px; + } + + .config-helper-text { + display: flex; + width: 100%; + justify-content: space-between; + } + + .bp5-form-group { + margin-bottom: 5px; + } } diff --git a/flow-editor/src/visual-node-editor/MacroInstanceEditor/MacroInstanceEditor.tsx b/flow-editor/src/visual-node-editor/MacroInstanceEditor/MacroInstanceEditor.tsx index f809d068a..805155475 100644 --- a/flow-editor/src/visual-node-editor/MacroInstanceEditor/MacroInstanceEditor.tsx +++ b/flow-editor/src/visual-node-editor/MacroInstanceEditor/MacroInstanceEditor.tsx @@ -1,4 +1,4 @@ -import { Button, Classes, Dialog, Intent } from "@blueprintjs/core"; +import { Button, Callout, Classes, Dialog, Intent } from "@blueprintjs/core"; import { MacroNodeDefinition, ResolvedDependenciesDefinitions, @@ -11,6 +11,8 @@ import { ErrorBoundary } from "react-error-boundary"; import React, { useMemo } from "react"; import { loadMacroEditor } from "./macroEditorLoader"; +import { InfoSign } from "@blueprintjs/icons"; +import { usePrompt } from "../../flow-editor/ports"; export interface MacroInstanceEditorProps { deps: ResolvedDependenciesDefinitions; @@ -42,16 +44,36 @@ export const MacroInstanceEditor: React.FC = ( return loadMacroEditor(macro as any as MacroNodeDefinition); }, [deps, ins]); + const prompt = usePrompt(); + return ( - +
- bob}> - + {macro.description ? ( + } + title={macro.displayName ?? macro.id} + > + {macro.description} + + ) : null} + + Error loading macro editor{" "} + + + } + > +
diff --git a/flow-editor/src/visual-node-editor/MacroInstanceEditor/SimpleJsonEditor.tsx b/flow-editor/src/visual-node-editor/MacroInstanceEditor/SimpleJsonEditor.tsx index 7e71704f3..9f7544b3a 100644 --- a/flow-editor/src/visual-node-editor/MacroInstanceEditor/SimpleJsonEditor.tsx +++ b/flow-editor/src/visual-node-editor/MacroInstanceEditor/SimpleJsonEditor.tsx @@ -4,7 +4,7 @@ import React, { useCallback } from "react"; export function SimpleJsonEditor(props: { value: any; onChange: (value: any) => void; - label: string; + label?: string; }) { const [tempDataValue, setTempDataValue] = React.useState( JSON.stringify(props.value, null, 2) diff --git a/flow-editor/src/visual-node-editor/MacroInstanceEditor/StructuredMacroEditorComp.tsx b/flow-editor/src/visual-node-editor/MacroInstanceEditor/StructuredMacroEditorComp.tsx new file mode 100644 index 000000000..487f09e13 --- /dev/null +++ b/flow-editor/src/visual-node-editor/MacroInstanceEditor/StructuredMacroEditorComp.tsx @@ -0,0 +1,30 @@ +import { MacroEditorComp, MacroEditorConfigStructured } from "@flyde/core"; +import { MacroConfigurableFieldEditor } from "@flyde/stdlib"; +import { usePrompt } from "../../flow-editor/ports"; + +export function StructuredMacroEditorComp( + editorConfig: MacroEditorConfigStructured +): MacroEditorComp { + return (props) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const prompt = usePrompt(); + return ( +
+ {editorConfig.fields.map((field) => ( + + props.onChange({ + ...props.value, + [field.configKey]: newValue, + }) + } + prompt={prompt} + config={field} + /> + ))} +
+ ); + }; +} diff --git a/flow-editor/src/visual-node-editor/MacroInstanceEditor/buildStructuredMacroEditor.tsx b/flow-editor/src/visual-node-editor/MacroInstanceEditor/buildStructuredMacroEditor.tsx deleted file mode 100644 index 60ecaca0e..000000000 --- a/flow-editor/src/visual-node-editor/MacroInstanceEditor/buildStructuredMacroEditor.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { - Checkbox, - FormGroup, - HTMLSelect, - InputGroup, - NumericInput, - TextArea, -} from "@blueprintjs/core"; -import { - MacroEditorComp, - MacroEditorConfigStructured, - MacroEditorFieldDefinition, - MacroEditorFieldDefinitionType, - MacroEditorFieldDefinitionTypeLongText, - MacroEditorFieldDefinitionTypeSelect, -} from "@flyde/core"; -import { SimpleJsonEditor } from "./SimpleJsonEditor"; -import { ConfigurableInputEditor } from "./ConfigurableInputEditor"; -import { useState, useEffect } from "react"; -import { usePrompt } from "../../flow-editor/ports"; - -export interface ValueCompProps { - value: T; - onChange: (value: T) => void; -} - -export function MacroEditorBaseValueComp( - props: ValueCompProps & { config: MacroEditorFieldDefinitionType } -) { - const _prompt = usePrompt(); - const [options, setOptions] = useState< - { value: string | number; label: string }[] - >((props.config as MacroEditorFieldDefinitionTypeSelect).items || []); - - useEffect(() => { - if ( - props.config.value === "select" && - !options.some((option) => option.value === props.value) - ) { - setOptions([ - ...options, - { value: props.value, label: String(props.value) }, - ]); - } - }, [props.value, options, props.config.value]); - - const handleAddOption = async () => { - const newOption = await _prompt("Enter a new option:"); - if (newOption && !options.some((option) => option.value === newOption)) { - const updatedOptions = [ - ...options, - { value: newOption, label: newOption }, - ]; - setOptions(updatedOptions); - props.onChange(newOption); - } - }; - - switch (props.config.value) { - case "number": - return ( - props.onChange(e)} - fill - /> - ); - case "string": - return ( - props.onChange(e.target.value)} - fill - /> - ); - case "json": - return ( - props.onChange(e)} - label={props.config.label} - /> - ); - case "boolean": - return ( - - props.onChange((e.target as HTMLInputElement).checked) - } - /> - ); - case "select": { - return ( - { - if (e.target.value === "__other__") { - handleAddOption(); - } else { - props.onChange(e.target.value); - } - }} - fill - > - {options.map((option) => ( - - ))} - - - ); - } - case "longtext": - return ( -