diff --git a/behavior3editor.code-workspace b/behavior3editor.code-workspace index 38d027f..d103805 100644 --- a/behavior3editor.code-workspace +++ b/behavior3editor.code-workspace @@ -6,6 +6,9 @@ ], "settings": { "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/public/locales/en.json b/public/locales/en.json index 2404254..b6e62a8 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -83,6 +83,9 @@ "regnode.subtree": "Subtree: ", "node.args": "Const arguments", + "node.args.add": "Add Sub Item", + "node.input.add": "Add Input Item", + "node.output.add": "Add Output Item", "node.children.unlimited": "Unlimited", "node.children": "Num children", "node.debug": "Debug", diff --git a/public/locales/zh.json b/public/locales/zh.json index d24297a..9952cce 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -83,6 +83,9 @@ "regnode.subtree": "子树:", "node.args": "常量参数", + "node.args.add": "添加子项", + "node.input.add": "添加输入项", + "node.output.add": "添加输出项", "node.children.unlimited": "无限制", "node.children": "子节点数量", "node.debug": "调试", diff --git a/src/components/explorer.tsx b/src/components/explorer.tsx index b1f021b..8edca9f 100644 --- a/src/components/explorer.tsx +++ b/src/components/explorer.tsx @@ -8,7 +8,7 @@ import Path from "@/misc/path"; import { DownOutlined } from "@ant-design/icons"; import { Button, Dropdown, Flex, FlexProps, Input, MenuProps, Space, Tree } from "antd"; import { ItemType } from "antd/es/menu/interface"; -import { ipcRenderer, shell } from "electron"; +import { ipcRenderer } from "electron"; import * as fs from "fs"; import React, { FC, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; diff --git a/src/components/inspector.tsx b/src/components/inspector.tsx index 021b57d..03a6f36 100644 --- a/src/components/inspector.tsx +++ b/src/components/inspector.tsx @@ -1,9 +1,15 @@ import { EditNode, EditTree, useWorkspace } from "@/contexts/workspace-context"; -import { NodeArg, NodeModel, TreeGraphData } from "@/misc/b3type"; -import { checkOneof } from "@/misc/b3util"; +import { NodeModel, TreeGraphData } from "@/misc/b3type"; +import { + checkNodeArgValue, + checkOneof, + getNodeArgRawType, + isNodeArgArray, + isNodeArgOptional, +} from "@/misc/b3util"; import { Hotkey, isMacos } from "@/misc/keys"; import { mergeClassNames } from "@/misc/util"; -import { EditOutlined } from "@ant-design/icons"; +import { EditOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; import { AutoComplete, Button, @@ -20,6 +26,7 @@ import { DefaultOptionType } from "antd/es/select"; import { FC, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import Markdown from "react-markdown"; +import { useDebounceCallback } from "usehooks-ts"; interface OptionType extends DefaultOptionType { value: string; @@ -91,6 +98,15 @@ const NodeInspector: FC = () => { const { t } = useTranslation(); const [form] = Form.useForm(); + const isVariadic = (arr: string[], i: number) => { + return arr[i].endsWith("...") && i === arr.length - 1; + }; + + const validateFieldsLater = useDebounceCallback( + () => form.validateFields({ recursive: true }), + 100 + ); + // set form values useEffect(() => { const data = workspace.editingNode.data; @@ -108,24 +124,45 @@ const NodeInspector: FC = () => { } else { form.setFieldValue("children", def.children); } - def.args?.forEach((v) => { - if (v.type === "json" || v.type === "json?") { - const value = data.args?.[v.name]; + def.args?.forEach((arg) => { + const type = getNodeArgRawType(arg); + const value = data.args?.[arg.name]; + const name = `args.${arg.name}`; + if (isNodeArgArray(arg)) { form.setFieldValue( - `args.${v.name}`, - value === null ? "null" : JSON.stringify(value ?? v.default, null, 2) + name, + (Array.isArray(value) ? value : []).map((item) => { + if (type === "json") { + return item === null ? "null" : JSON.stringify(item ?? arg.default, null, 2); + } else { + return item; + } + }) + ); + } else if (type === "json") { + form.setFieldValue( + name, + value === null ? "null" : JSON.stringify(value ?? arg.default, null, 2) ); } else { - form.setFieldValue(`args.${v.name}`, data.args?.[v.name] ?? v.default); + form.setFieldValue(name, value ?? arg.default); } }); def.input?.forEach((_, i) => { - form.setFieldValue(`input.${i}`, data.input?.[i]); + if (isVariadic(def.input!, i)) { + form.setFieldValue(`input.${i}`, data.input?.slice(i) ?? []); + } else { + form.setFieldValue(`input.${i}`, data.input?.[i]); + } }); def.output?.forEach((_, i) => { - form.setFieldValue(`output.${i}`, data.output?.[i]); + if (isVariadic(def.output!, i)) { + form.setFieldValue(`output.${i}`, data.output?.slice(i) ?? []); + } else { + form.setFieldValue(`output.${i}`, data.output?.[i]); + } }); - form.validateFields(); + validateFieldsLater(); }, [workspace.editingNode]); // auto complete for node @@ -143,13 +180,25 @@ const NodeInspector: FC = () => { const collect = (node?: TreeGraphData) => { if (node) { node.input?.forEach((v, i) => { - const desc = node.def.input?.[i] ?? ""; + let desc: string; + const inputDef = node.def.input; + if (inputDef && i >= inputDef.length && isVariadic(inputDef, inputDef.length - 1)) { + desc = inputDef[inputDef.length - 1]; + } else { + desc = inputDef?.[i] ?? ""; + } if (v && !options.find((option) => option.value === v)) { options.push({ label: `${v}(${desc})`, value: v }); } }); node.output?.forEach((v, i) => { - const desc = node.def.output?.[i] ?? ""; + let desc: string; + const outputDef = node.def.output; + if (outputDef && i >= outputDef.length && isVariadic(outputDef, outputDef.length - 1)) { + desc = outputDef[outputDef.length - 1]; + } else { + desc = outputDef?.[i] ?? ""; + } if (v && !options.find((option) => option.value === v)) { options.push({ label: `${v}(${desc})`, value: v }); } @@ -192,13 +241,34 @@ const NodeInspector: FC = () => { if (def.args?.length) { def.args?.forEach((arg) => { - const v = values[`args.${arg.name}`]; - if (v !== null && v !== undefined && v !== "") { + const value = values[`args.${arg.name}`]; + if (value !== null && value !== undefined && value !== "") { data.args ||= {}; - if (arg.type === "json" || arg.type === "json?") { - data.args[arg.name] = v === "null" ? null : JSON.parse(v); + const type = getNodeArgRawType(arg); + if (isNodeArgArray(arg)) { + const arr: unknown[] = []; + if (Array.isArray(value)) { + value.forEach((item) => { + if (type === "json") { + try { + arr.push(item === "null" ? null : JSON.parse(item)); + } catch { + /** ignore */ + } + } else if (item !== null && item !== undefined) { + arr.push(item); + } + }); + } + data.args[arg.name] = arr; + } else if (type === "json") { + try { + data.args[arg.name] = value === "null" ? null : JSON.parse(value); + } catch { + /** ignore */ + } } else { - data.args[arg.name] = v; + data.args[arg.name] = value; } } }); @@ -208,9 +278,14 @@ const NodeInspector: FC = () => { if (def.input?.length) { def.input?.forEach((_, i) => { - const v = values[`input.${i}`]; data.input ||= []; - data.input.push(v ?? ""); + if (isVariadic(def.input!, i)) { + const arr = (values[`input.${i}`] ?? []) as string[]; + data.input.push(...arr.filter((v) => typeof v === "string")); + } else { + const v = values[`input.${i}`]; + data.input.push(v ?? ""); + } }); } else { data.input = []; @@ -218,9 +293,14 @@ const NodeInspector: FC = () => { if (def.output?.length) { def.output?.forEach((_, i) => { - const v = values[`output.${i}`]; data.output ||= []; - data.output.push(v ?? ""); + if (isVariadic(def.output!, i)) { + const arr = (values[`output.${i}`] ?? []) as string[]; + data.output.push(...arr.filter((v) => typeof v === "string")); + } else { + const v = values[`output.${i}`]; + data.output.push(v ?? ""); + } }); } else { data.output = []; @@ -328,56 +408,119 @@ const NodeInspector: FC = () => { {def.input.map((v, i) => { const required = v.indexOf("?") === -1; const desc = v.replace("?", ""); - return ( - ({ - validator(_, value) { - const arg = def.args?.find( - (a) => a.oneof && v.replace("?", "") === a.oneof - ); - if (arg) { - const argName = `args.${arg.name}`; - if (!isFieldValidating(argName)) { - setFieldValue(`input.${i}`, value); - validateFields([argName]); + if (isVariadic(def.input!, i)) { + return ( + + + {(fields, { add, remove }, { errors }) => ( +
+ {fields.map((field) => ( + + + + e.code === Hotkey.Escape && e.preventDefault() + } + filterOption={(inputValue: string, option?: OptionType) => { + const label = option!.label as string; + return ( + label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + ); + }} + /> + + { + remove(field.name); + form.submit(); + }} + /> + + ))} + + + + +
+ )} +
+
+ ); + } else { + return ( + ({ + validator(_, value) { + const arg = def.args?.find( + (a) => a.oneof && v.replace("?", "") === a.oneof + ); + if (arg) { + const argName = `args.${arg.name}`; + if (!isFieldValidating(argName)) { + setFieldValue(`input.${i}`, value); + validateFields([argName]); + } + if (!checkOneof(getFieldValue(argName) ?? "", value)) { + return Promise.reject( + new Error( + t("node.oneof.error", { + input: v, + arg: arg.name, + desc: arg.desc ?? "", + }) + ) + ); + } else { + return Promise.resolve(); + } } - if (!checkOneof(getFieldValue(argName) ?? "", value)) { - return Promise.reject( - new Error( - t("node.oneof.error", { - input: v, - arg: arg.name, - desc: arg.desc ?? "", - }) - ) - ); - } else { - return Promise.resolve(); - } - } - return Promise.resolve(); - }, - }), - ]} - > - e.code === Hotkey.Escape && e.preventDefault()} - filterOption={(inputValue: string, option?: OptionType) => { - const label = option!.label as string; - return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1; - }} - /> - - ); + return Promise.resolve(); + }, + }), + ]} + > + e.code === Hotkey.Escape && e.preventDefault()} + filterOption={(inputValue: string, option?: OptionType) => { + const label = option!.label as string; + return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1; + }} + /> +
+ ); + } })} )} @@ -386,87 +529,207 @@ const NodeInspector: FC = () => {

{t("node.args")}

- {def.args.map((v) => { - const required = v.type.indexOf("?") === -1; - const type = v.type.replace("?", "") as NodeArg["type"]; - return ( - ({ - validator(_, value) { - if (value && (v.type === "json" || v.type === "json?")) { - try { - if (value !== "null") { - JSON.parse(value); + {def.args.map((arg) => { + const required = !isNodeArgOptional(arg); + const type = getNodeArgRawType(arg); + if (isNodeArgArray(arg)) { + return ( + + + {(items, { add, remove }, { errors }) => ( +
+ {items.map((item, idx) => ( + + ({ + validator(_, value) { + if (type === "json") { + try { + if (value !== "null") { + JSON.parse(value); + } + } catch (e) { + return Promise.reject( + new Error(t("node.invalidValue")) + ); + } + } else if ( + !checkNodeArgValue(editingNode.data, arg, value, true) + ) { + return Promise.reject(new Error(t("node.invalidValue"))); + } + return Promise.resolve(); + }, + }), + ]} + > + {type === "string" && ( +