support array type for node args

This commit is contained in:
codetypes committed 2024-09-08 10:41:58 +08:00
1 parent d7a2ce0f9b
commit f2d0686707
9 files changed
+545 -137

No files matched your search

+3
View File
@@ -6,6 +6,9 @@
], ],
"settings": { "settings": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} }
} }
+3
View File
@@ -83,6 +83,9 @@
"regnode.subtree": "Subtree: ", "regnode.subtree": "Subtree: ",
"node.args": "Const arguments", "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.unlimited": "Unlimited",
"node.children": "Num children", "node.children": "Num children",
"node.debug": "Debug", "node.debug": "Debug",
+3
View File
@@ -83,6 +83,9 @@
"regnode.subtree": "子树:", "regnode.subtree": "子树:",
"node.args": "常量参数", "node.args": "常量参数",
"node.args.add": "添加子项",
"node.input.add": "添加输入项",
"node.output.add": "添加输出项",
"node.children.unlimited": "无限制", "node.children.unlimited": "无限制",
"node.children": "子节点数量", "node.children": "子节点数量",
"node.debug": "调试", "node.debug": "调试",
+1 -1
View File
@@ -8,7 +8,7 @@ import Path from "@/misc/path";
import { DownOutlined } from "@ant-design/icons"; import { DownOutlined } from "@ant-design/icons";
import { Button, Dropdown, Flex, FlexProps, Input, MenuProps, Space, Tree } from "antd"; import { Button, Dropdown, Flex, FlexProps, Input, MenuProps, Space, Tree } from "antd";
import { ItemType } from "antd/es/menu/interface"; import { ItemType } from "antd/es/menu/interface";
import { ipcRenderer, shell } from "electron"; import { ipcRenderer } from "electron";
import * as fs from "fs"; import * as fs from "fs";
import React, { FC, useEffect, useMemo, useState } from "react"; import React, { FC, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
+364 -38
View File
@@ -1,9 +1,15 @@
import { EditNode, EditTree, useWorkspace } from "@/contexts/workspace-context"; import { EditNode, EditTree, useWorkspace } from "@/contexts/workspace-context";
import { NodeArg, NodeModel, TreeGraphData } from "@/misc/b3type"; import { NodeModel, TreeGraphData } from "@/misc/b3type";
import { checkOneof } from "@/misc/b3util"; import {
checkNodeArgValue,
checkOneof,
getNodeArgRawType,
isNodeArgArray,
isNodeArgOptional,
} from "@/misc/b3util";
import { Hotkey, isMacos } from "@/misc/keys"; import { Hotkey, isMacos } from "@/misc/keys";
import { mergeClassNames } from "@/misc/util"; import { mergeClassNames } from "@/misc/util";
import { EditOutlined } from "@ant-design/icons"; import { EditOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { import {
AutoComplete, AutoComplete,
Button, Button,
@@ -20,6 +26,7 @@ import { DefaultOptionType } from "antd/es/select";
import { FC, useEffect, useMemo } from "react"; import { FC, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { useDebounceCallback } from "usehooks-ts";
interface OptionType extends DefaultOptionType { interface OptionType extends DefaultOptionType {
value: string; value: string;
@@ -91,6 +98,15 @@ const NodeInspector: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); 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 // set form values
useEffect(() => { useEffect(() => {
const data = workspace.editingNode.data; const data = workspace.editingNode.data;
@@ -108,24 +124,45 @@ const NodeInspector: FC = () => {
} else { } else {
form.setFieldValue("children", def.children); form.setFieldValue("children", def.children);
} }
def.args?.forEach((v) => { def.args?.forEach((arg) => {
if (v.type === "json" || v.type === "json?") { const type = getNodeArgRawType(arg);
const value = data.args?.[v.name]; const value = data.args?.[arg.name];
const name = `args.${arg.name}`;
if (isNodeArgArray(arg)) {
form.setFieldValue( form.setFieldValue(
`args.${v.name}`, name,
value === null ? "null" : JSON.stringify(value ?? v.default, null, 2) (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 { } else {
form.setFieldValue(`args.${v.name}`, data.args?.[v.name] ?? v.default); form.setFieldValue(name, value ?? arg.default);
} }
}); });
def.input?.forEach((_, i) => { def.input?.forEach((_, i) => {
if (isVariadic(def.input!, i)) {
form.setFieldValue(`input.${i}`, data.input?.slice(i) ?? []);
} else {
form.setFieldValue(`input.${i}`, data.input?.[i]); form.setFieldValue(`input.${i}`, data.input?.[i]);
}
}); });
def.output?.forEach((_, i) => { def.output?.forEach((_, i) => {
if (isVariadic(def.output!, i)) {
form.setFieldValue(`output.${i}`, data.output?.slice(i) ?? []);
} else {
form.setFieldValue(`output.${i}`, data.output?.[i]); form.setFieldValue(`output.${i}`, data.output?.[i]);
}
}); });
form.validateFields(); validateFieldsLater();
}, [workspace.editingNode]); }, [workspace.editingNode]);
// auto complete for node // auto complete for node
@@ -143,13 +180,25 @@ const NodeInspector: FC = () => {
const collect = (node?: TreeGraphData) => { const collect = (node?: TreeGraphData) => {
if (node) { if (node) {
node.input?.forEach((v, i) => { node.input?.forEach((v, i) => {
const desc = node.def.input?.[i] ?? "<unknown>"; 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] ?? "<unknown>";
}
if (v && !options.find((option) => option.value === v)) { if (v && !options.find((option) => option.value === v)) {
options.push({ label: `${v}(${desc})`, value: v }); options.push({ label: `${v}(${desc})`, value: v });
} }
}); });
node.output?.forEach((v, i) => { node.output?.forEach((v, i) => {
const desc = node.def.output?.[i] ?? "<unknown>"; 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] ?? "<unknown>";
}
if (v && !options.find((option) => option.value === v)) { if (v && !options.find((option) => option.value === v)) {
options.push({ label: `${v}(${desc})`, value: v }); options.push({ label: `${v}(${desc})`, value: v });
} }
@@ -192,13 +241,34 @@ const NodeInspector: FC = () => {
if (def.args?.length) { if (def.args?.length) {
def.args?.forEach((arg) => { def.args?.forEach((arg) => {
const v = values[`args.${arg.name}`]; const value = values[`args.${arg.name}`];
if (v !== null && v !== undefined && v !== "") { if (value !== null && value !== undefined && value !== "") {
data.args ||= {}; data.args ||= {};
if (arg.type === "json" || arg.type === "json?") { const type = getNodeArgRawType(arg);
data.args[arg.name] = v === "null" ? null : JSON.parse(v); 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 { } else {
data.args[arg.name] = v; data.args[arg.name] = value;
} }
} }
}); });
@@ -208,9 +278,14 @@ const NodeInspector: FC = () => {
if (def.input?.length) { if (def.input?.length) {
def.input?.forEach((_, i) => { def.input?.forEach((_, i) => {
const v = values[`input.${i}`];
data.input ||= []; data.input ||= [];
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 ?? ""); data.input.push(v ?? "");
}
}); });
} else { } else {
data.input = []; data.input = [];
@@ -218,9 +293,14 @@ const NodeInspector: FC = () => {
if (def.output?.length) { if (def.output?.length) {
def.output?.forEach((_, i) => { def.output?.forEach((_, i) => {
const v = values[`output.${i}`];
data.output ||= []; data.output ||= [];
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 ?? ""); data.output.push(v ?? "");
}
}); });
} else { } else {
data.output = []; data.output = [];
@@ -328,6 +408,68 @@ const NodeInspector: FC = () => {
{def.input.map((v, i) => { {def.input.map((v, i) => {
const required = v.indexOf("?") === -1; const required = v.indexOf("?") === -1;
const desc = v.replace("?", ""); const desc = v.replace("?", "");
if (isVariadic(def.input!, i)) {
return (
<Form.Item label={desc} key={`input.${i}`}>
<Form.List name={`input.${i}`}>
{(fields, { add, remove }, { errors }) => (
<div
style={{
display: "flex",
rowGap: 0,
flexDirection: "column",
}}
>
{fields.map((field) => (
<Flex key={field.key} gap={4}>
<Form.Item
name={field.name}
validateTrigger={["onChange", "onBlur"]}
style={{ width: "100%", marginBottom: 5 }}
>
<AutoComplete
disabled={disabled}
options={inoutVarOptions}
onBlur={form.submit}
onInputKeyDown={(e) =>
e.code === Hotkey.Escape && e.preventDefault()
}
filterOption={(inputValue: string, option?: OptionType) => {
const label = option!.label as string;
return (
label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
);
}}
/>
</Form.Item>
<MinusCircleOutlined
style={{ marginBottom: "6px" }}
onClick={() => {
remove(field.name);
form.submit();
}}
/>
</Flex>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add("");
}}
style={{ width: fields.length === 0 ? "100%" : "200px" }}
icon={<PlusOutlined />}
>
{t("node.input.add")}
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</div>
)}
</Form.List>
</Form.Item>
);
} else {
return ( return (
<Form.Item <Form.Item
label={desc} label={desc}
@@ -378,6 +520,7 @@ const NodeInspector: FC = () => {
/> />
</Form.Item> </Form.Item>
); );
}
})} })}
</> </>
)} )}
@@ -386,21 +529,137 @@ const NodeInspector: FC = () => {
<Divider orientation="left"> <Divider orientation="left">
<h4>{t("node.args")}</h4> <h4>{t("node.args")}</h4>
</Divider> </Divider>
{def.args.map((v) => { {def.args.map((arg) => {
const required = v.type.indexOf("?") === -1; const required = !isNodeArgOptional(arg);
const type = v.type.replace("?", "") as NodeArg["type"]; const type = getNodeArgRawType(arg);
if (isNodeArgArray(arg)) {
return ( return (
<Form.Item key={`args.${arg.name}`}>
<Form.List name={`args.${arg.name}`}>
{(items, { add, remove }, { errors }) => (
<div style={{ display: "flex", rowGap: 0, flexDirection: "column" }}>
{items.map((item, idx) => (
<Flex key={item.key} gap={4}>
<Form.Item <Form.Item
name={`args.${v.name}`} name={item.name}
label={v.desc} label={idx === 0 ? `${arg.desc}[${idx}]` : `[${idx}]`}
key={`args.${v.name}`} validateTrigger={["onChange", "onBlur"]}
initialValue={type === "boolean" ? v.default ?? false : v.default} style={{ width: "100%", marginBottom: 5 }}
initialValue={
type === "boolean" ? arg.default ?? false : arg.default
}
valuePropName={type === "boolean" ? "checked" : undefined} valuePropName={type === "boolean" ? "checked" : undefined}
rules={[ rules={[
{ required, message: t("node.fileRequired", { field: v.desc }) }, () => ({
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" && (
<TextArea autoSize disabled={disabled} onBlur={form.submit} />
)}
{type === "json" && (
<TextArea autoSize disabled={disabled} onBlur={form.submit} />
)}
{type === "int" && (
<InputNumber
disabled={disabled}
onBlur={form.submit}
precision={0}
/>
)}
{type === "float" && (
<InputNumber disabled={disabled} onBlur={form.submit} />
)}
{type === "boolean" && (
<Switch disabled={disabled} onChange={form.submit} />
)}
{type === "code" && (
<Input disabled={disabled} onBlur={form.submit} />
)}
{type === "enum" && (
<Select
disabled={disabled}
onBlur={form.submit}
onChange={form.submit}
>
{arg.options?.map((value, i) => {
return (
<Select.Option key={i} value={value.value}>
{value.name}
</Select.Option>
);
})}
</Select>
)}
</Form.Item>
<MinusCircleOutlined
style={{ marginBottom: "6px" }}
onClick={() => {
remove(item.name);
form.submit();
}}
/>
</Flex>
))}
<Form.Item
label={items.length === 0 ? arg.desc : undefined}
style={{
marginLeft: items.length === 0 ? undefined : "100px",
marginRight: items.length === 0 ? undefined : "18px",
alignItems: "end",
}}
>
<Button
type="dashed"
onClick={() => {
add(arg.default ?? type === "boolean" ? false : "");
if (type === "boolean") {
form.submit();
}
}}
style={{ width: "100%" }}
icon={<PlusOutlined />}
>
{t("node.args.add")}
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</div>
)}
</Form.List>
</Form.Item>
);
} else {
return (
<Form.Item
name={`args.${arg.name}`}
key={`args.${arg.name}`}
label={arg.desc}
initialValue={type === "boolean" ? arg.default ?? false : arg.default}
valuePropName={type === "boolean" ? "checked" : undefined}
rules={[
{ required, message: t("node.fileRequired", { field: arg.desc }) },
({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({ ({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({
validator(_, value) { validator(_, value) {
if (value && (v.type === "json" || v.type === "json?")) { if (value && type === "json") {
try { try {
if (value !== "null") { if (value !== "null") {
JSON.parse(value); JSON.parse(value);
@@ -409,20 +668,23 @@ const NodeInspector: FC = () => {
return Promise.reject(new Error(t("node.invalidValue"))); return Promise.reject(new Error(t("node.invalidValue")));
} }
} }
if (!v.oneof) { if (!checkNodeArgValue(editingNode.data, arg, value)) {
return Promise.reject(new Error(t("node.invalidValue")));
}
if (!arg.oneof) {
return Promise.resolve(); return Promise.resolve();
} }
const idx = def.input?.findIndex( const idx = def.input?.findIndex(
(input) => input.replace("?", "") === v.oneof (input) => input.replace("?", "") === arg.oneof
); );
if (idx === undefined || idx < 0) { if (idx === undefined || idx < 0) {
return Promise.reject( return Promise.reject(
new Error(t("node.oneof.inputNotfound", { input: v.oneof })) new Error(t("node.oneof.inputNotfound", { input: arg.oneof }))
); );
} }
const inputName = `input.${idx}`; const inputName = `input.${idx}`;
if (!isFieldValidating(inputName)) { if (!isFieldValidating(inputName)) {
setFieldValue(`args.${v.name}`, value); setFieldValue(`args.${arg.name}`, value);
validateFields([inputName]); validateFields([inputName]);
} }
if (!checkOneof(getFieldValue(inputName) ?? "", value)) { if (!checkOneof(getFieldValue(inputName) ?? "", value)) {
@@ -430,8 +692,8 @@ const NodeInspector: FC = () => {
new Error( new Error(
t("node.oneof.error", { t("node.oneof.error", {
input: def.input![idx], input: def.input![idx],
arg: v.name, arg: arg.name,
desc: v.desc ?? "", desc: arg.desc ?? "",
}) })
) )
); );
@@ -456,7 +718,7 @@ const NodeInspector: FC = () => {
{type === "code" && <Input disabled={disabled} onBlur={form.submit} />} {type === "code" && <Input disabled={disabled} onBlur={form.submit} />}
{type === "enum" && ( {type === "enum" && (
<Select disabled={disabled} onBlur={form.submit} onChange={form.submit}> <Select disabled={disabled} onBlur={form.submit} onChange={form.submit}>
{v.options?.map((value, index) => { {arg.options?.map((value, index) => {
return ( return (
<Select.Option key={index} value={value.value}> <Select.Option key={index} value={value.value}>
{value.name} {value.name}
@@ -467,6 +729,7 @@ const NodeInspector: FC = () => {
)} )}
</Form.Item> </Form.Item>
); );
}
})} })}
</> </>
)} )}
@@ -478,6 +741,68 @@ const NodeInspector: FC = () => {
{def.output.map((v, i) => { {def.output.map((v, i) => {
const required = v.indexOf("?") === -1; const required = v.indexOf("?") === -1;
const desc = v.replace("?", ""); const desc = v.replace("?", "");
if (isVariadic(def.output!, i)) {
return (
<Form.Item label={desc} key={`output.${i}`}>
<Form.List name={`output.${i}`}>
{(fields, { add, remove }, { errors }) => (
<div
style={{
display: "flex",
rowGap: 0,
flexDirection: "column",
}}
>
{fields.map((field) => (
<Flex key={field.key} gap={4}>
<Form.Item
name={field.name}
validateTrigger={["onChange", "onBlur"]}
style={{ width: "100%", marginBottom: 5 }}
>
<AutoComplete
disabled={disabled}
options={inoutVarOptions}
onBlur={form.submit}
onInputKeyDown={(e) =>
e.code === Hotkey.Escape && e.preventDefault()
}
filterOption={(value: string, option?: OptionType) => {
const label = option!.label as string;
return (
label.toUpperCase().indexOf(value.toUpperCase()) !== -1
);
}}
/>
</Form.Item>
<MinusCircleOutlined
style={{ marginBottom: "6px" }}
onClick={() => {
remove(field.name);
form.submit();
}}
/>
</Flex>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add("");
}}
style={{ width: fields.length === 0 ? "100%" : "200px" }}
icon={<PlusOutlined />}
>
{t("node.output.add")}
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</div>
)}
</Form.List>
</Form.Item>
);
} else {
return ( return (
<Form.Item <Form.Item
label={desc} label={desc}
@@ -490,13 +815,14 @@ const NodeInspector: FC = () => {
options={inoutVarOptions} options={inoutVarOptions}
onBlur={form.submit} onBlur={form.submit}
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()} onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
filterOption={(inputValue: string, option?: OptionType) => { filterOption={(value: string, option?: OptionType) => {
const label = option!.label as string; const label = option!.label as string;
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1; return label.toUpperCase().indexOf(value.toUpperCase()) !== -1;
}} }}
/> />
</Form.Item> </Form.Item>
); );
}
})} })}
</> </>
)} )}
@@ -670,7 +996,7 @@ export const Inspector: FC = () => {
<Flex <Flex
vertical vertical
className="b3-inspector" className="b3-inspector"
style={{ height: "100%", width: "340px", borderLeft: `1px solid var(--b3-color-border)` }} style={{ height: "100%", width: "360px", borderLeft: `1px solid var(--b3-color-border)` }}
> >
{isEditingNodeDef && <NodeDefInspector />} {isEditingNodeDef && <NodeDefInspector />}
{isEditingTree && <TreeInspector />} {isEditingTree && <TreeInspector />}
+1 -1
View File
@@ -1,11 +1,11 @@
import { useWorkspace } from "@/contexts/workspace-context"; import { useWorkspace } from "@/contexts/workspace-context";
import { NodeDef } from "@/misc/b3type";
import { Hotkey, isMacos } from "@/misc/keys"; import { Hotkey, isMacos } from "@/misc/keys";
import Path from "@/misc/path"; import Path from "@/misc/path";
import { SearchOutlined } from "@ant-design/icons"; import { SearchOutlined } from "@ant-design/icons";
import { Button, Flex, LayoutProps, Select } from "antd"; import { Button, Flex, LayoutProps, Select } from "antd";
import React, { FC, useMemo } from "react"; import React, { FC, useMemo } from "react";
import { Menu } from "./menu"; import { Menu } from "./menu";
import { NodeDef } from "@/misc/b3type";
interface OptionType { interface OptionType {
label: string | React.ReactNode; label: string | React.ReactNode;
+2 -3
View File
@@ -2,16 +2,15 @@ import { NodeDef, NodeModel, TreeGraphData, TreeModel, unknownNodeDef } from "@/
import * as b3util from "@/misc/b3util"; import * as b3util from "@/misc/b3util";
import { message } from "@/misc/hooks"; import { message } from "@/misc/hooks";
import i18n from "@/misc/i18n"; import i18n from "@/misc/i18n";
import { readJson, writeJson } from "@/misc/util";
import Path from "@/misc/path"; import Path from "@/misc/path";
import { readJson, writeJson } from "@/misc/util";
import { Matrix, TreeGraph } from "@antv/g6"; import { Matrix, TreeGraph } from "@antv/g6";
import { BrowserWindow, dialog } from "@electron/remote"; import { BrowserWindow, dialog } from "@electron/remote";
import { ipcRenderer } from "electron";
import * as fs from "fs"; import * as fs from "fs";
import React from "react"; import React from "react";
import { create } from "zustand"; import { create } from "zustand";
import { useSetting } from "./setting-context"; import { useSetting } from "./setting-context";
import { ipcRenderer } from "electron";
import { Editor } from "../components/editor";
let buildDir: string | undefined; let buildDir: string | undefined;
+118 -57
View File
@@ -1,11 +1,11 @@
import { useWorkspace } from "@/contexts/workspace-context"; import { useWorkspace } from "@/contexts/workspace-context";
import { NodeArg, NodeDef, NodeModel, TreeGraphData, TreeModel } from "@/misc/b3type"; import { NodeArg, NodeDef, NodeModel, TreeGraphData, TreeModel } from "@/misc/b3type";
import i18n from "@/misc/i18n";
import * as fs from "fs"; import * as fs from "fs";
import { message } from "./hooks"; import { message } from "./hooks";
import { isMacos } from "./keys";
import Path from "./path"; import Path from "./path";
import { zhNodeDef } from "./template"; import { zhNodeDef } from "./template";
import i18n from "@/misc/i18n";
import { isMacos } from "./keys";
let ctx: CanvasRenderingContext2D | null = null; let ctx: CanvasRenderingContext2D | null = null;
let defaultFontSize = ""; let defaultFontSize = "";
@@ -110,7 +110,86 @@ const error = (data: NodeModel | TreeGraphData, msg: string) => {
console.error(`check ${data.id}|${data.name}: ${msg}`); console.error(`check ${data.id}|${data.name}: ${msg}`);
}; };
const checkNodeArg = ( export const getNodeArgRawType = (arg: NodeArg) => {
return arg.type.match(/^\w+/)![0] as NodeArg["type"];
};
export const isNodeArgArray = (arg: NodeArg) => {
return arg.type.includes("[]");
};
export const isNodeArgOptional = (arg: NodeArg) => {
return arg.type.includes("?");
};
export const checkNodeArgValue = (
data: NodeModel | TreeGraphData,
arg: NodeArg,
value: unknown,
verbose?: boolean
) => {
let hasError = false;
const type = getNodeArgRawType(arg);
if (type === "float") {
const isNumber = typeof value === "number";
const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(isNumber || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a number`);
}
hasError = true;
}
} else if (type === "int") {
const isInt = typeof value === "number" && value === Math.floor(value);
const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(isInt || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a int`);
}
hasError = true;
}
} else if (type === "string") {
const isString = typeof value === "string" && value;
const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg);
if (!(isString || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a string`);
}
hasError = true;
}
} else if (type === "enum") {
const isEnum = !!arg.options?.find((option) => option.value === value);
const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(isEnum || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`);
}
hasError = true;
}
} else if (type === "code") {
const isCode = typeof value === "string" && value;
const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg);
if (!(isCode || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a string`);
}
hasError = true;
}
} else if (type === "json") {
const isJson = value !== undefined && value !== "";
const isOptional = isNodeArgOptional(arg);
if (!(isJson || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${value}' is not an invalid object`);
}
hasError = true;
}
}
return !hasError;
};
export const checkNodeArg = (
data: NodeModel | TreeGraphData, data: NodeModel | TreeGraphData,
conf: NodeDef, conf: NodeDef,
i: number, i: number,
@@ -118,63 +197,23 @@ const checkNodeArg = (
) => { ) => {
let hasError = false; let hasError = false;
const arg = conf.args![i] as NodeArg; const arg = conf.args![i] as NodeArg;
const type = arg.type.replace("?", "") as NodeArg["type"];
const value = data.args?.[arg.name]; const value = data.args?.[arg.name];
if (type === "float") { if (isNodeArgArray(arg)) {
const isNumber = typeof value === "number"; if (!Array.isArray(value)) {
const isOptional = value === undefined && arg.type === "float?";
if (!(isNumber || isOptional)) {
if (verbose) { if (verbose) {
error(data, `'${arg.name}=${value}' is not a number`); error(data, `'${arg.name}=${value}' is not an array`);
} }
hasError = true; hasError = true;
} } else {
} else if (type === "int") { for (let j = 0; j < value.length; j++) {
const isInt = typeof value === "number" && value === Math.floor(value); if (!checkNodeArgValue(data, arg, value[j], verbose)) {
const isOptional = value === undefined && arg.type === "int?";
if (!(isInt || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${value}' is not a int`);
}
hasError = true; hasError = true;
} }
} else if (type === "string") {
const isString = typeof value === "string" && value;
const isOptional = (value === undefined || value === "") && arg.type === "string?";
if (!(isString || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${value}' is not a string`);
} }
}
} else if (!checkNodeArgValue(data, arg, value, verbose)) {
hasError = true; hasError = true;
} }
} else if (type === "enum") {
const isEnum = !!arg.options?.find((option) => option.value === value);
const isOptional = value === undefined && arg.type === "enum?";
if (!(isEnum || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${value}' is not a one of the option values`);
}
hasError = true;
}
} else if (type === "code") {
const isCode = typeof value === "string" && value;
const isOptional = (value === undefined || value === "") && arg.type === "code?";
if (!(isCode || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${value}' is not a string`);
}
hasError = true;
}
} else if (type === "json") {
const isJson = value !== undefined && value !== "";
const isOptional = arg.type === "json?";
if (!(isJson || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${value}' is not an invalid object`);
}
hasError = true;
}
}
if (arg.oneof !== undefined) { if (arg.oneof !== undefined) {
const idx = conf.input?.findIndex((v) => v.startsWith(arg.oneof!)) ?? -1; const idx = conf.input?.findIndex((v) => v.startsWith(arg.oneof!)) ?? -1;
if (!checkOneof(data.args?.[arg.name], data.input?.[idx])) { if (!checkOneof(data.args?.[arg.name], data.input?.[idx])) {
@@ -210,6 +249,8 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
error(data, `expect ${conf.children} children, but got ${count}`); error(data, `expect ${conf.children} children, but got ${count}`);
} }
} }
let hasVaridicInput = false;
if (conf.input) { if (conf.input) {
for (let i = 0; i < conf.input.length; i++) { for (let i = 0; i < conf.input.length; i++) {
if (!data.input) { if (!data.input) {
@@ -218,15 +259,20 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
if (!data.input[i]) { if (!data.input[i]) {
data.input[i] = ""; data.input[i] = "";
} }
if (conf.input[i].indexOf("?") === -1 && !data.input[i]) { if (!isValidInputOrOutput(conf.input, data.input, i)) {
error(data, `intput field '${conf.input[i]}' is required`); error(data, `intput field '${conf.input[i]}' is required`);
hasError = true; hasError = true;
} }
if (i === conf.input.length - 1 && conf.input.at(-1)?.endsWith("...")) {
hasVaridicInput = true;
} }
} }
if (data.input) { }
if (data.input && !hasVaridicInput) {
data.input.length = conf.input?.length || 0; data.input.length = conf.input?.length || 0;
} }
let hasVaridicOutput = false;
if (conf.output) { if (conf.output) {
for (let i = 0; i < conf.output.length; i++) { for (let i = 0; i < conf.output.length; i++) {
if (!data.output) { if (!data.output) {
@@ -235,13 +281,16 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
if (!data.output[i]) { if (!data.output[i]) {
data.output[i] = ""; data.output[i] = "";
} }
if (conf.output[i].indexOf("?") === -1 && !data.output[i]) { if (!isValidInputOrOutput(conf.output, data.output, i)) {
error(data, `output field '${conf.output[i]}' is required`); error(data, `output field '${conf.output[i]}' is required`);
hasError = true; hasError = true;
} }
if (i === conf.output.length - 1 && conf.output.at(-1)?.endsWith("...")) {
hasVaridicOutput = true;
} }
} }
if (data.output) { }
if (data.output && !hasVaridicOutput) {
data.output.length = conf.output?.length || 0; data.output.length = conf.output?.length || 0;
} }
if (conf.args) { if (conf.args) {
@@ -456,18 +505,30 @@ export const checkChildrenLimit = (data: TreeGraphData) => {
return true; return true;
}; };
const isValidInputOrOutput = (
inputDef: string[],
inputData: string[] | undefined,
index: number
) => {
return (
inputDef[index].includes("?") ||
inputData?.[index] ||
(index === inputDef.length - 1 && inputDef[index].endsWith("..."))
);
};
export const checkTreeData = (data: TreeGraphData) => { export const checkTreeData = (data: TreeGraphData) => {
const conf = data.def; const conf = data.def;
if (conf.input) { if (conf.input) {
for (let i = 0; i < conf.input.length; i++) { for (let i = 0; i < conf.input.length; i++) {
if (conf.input[i].indexOf("?") === -1 && !data.input?.[i]) { if (!isValidInputOrOutput(conf.input, data.input, i)) {
return false; return false;
} }
} }
} }
if (conf.output) { if (conf.output) {
for (let i = 0; i < conf.output.length; i++) { for (let i = 0; i < conf.output.length; i++) {
if (conf.output[i].indexOf("?") === -1 && !data.output?.[i]) { if (!isValidInputOrOutput(conf.output, data.output, i)) {
return false; return false;
} }
} }
+50 -37
View File
@@ -32,34 +32,6 @@ export const zhNodeDef = () => {
], ],
doc: "+ 只能有一个子节点,多个仅执行第一个\n+ 当子节点返回 `failure` 时,抛出异常\n+ 其余情况返回子节点的执行状态\n", doc: "+ 只能有一个子节点,多个仅执行第一个\n+ 当子节点返回 `failure` 时,抛出异常\n+ 其余情况返回子节点的执行状态\n",
}, },
{
name: "Assign",
type: "Action",
children: 0,
status: ["success"],
desc: "对输入对象设置 key 和 value",
input: ["输入对象", "输入key?", "输入value?"],
args: [
{
name: "key",
type: "string?",
desc: "常量key",
oneof: "输入key",
},
{
name: "value",
type: "json?",
desc: "常量value",
oneof: "输入value",
},
{
name: "underscore",
type: "boolean?",
desc: "允许下划线",
},
],
doc: "+ 对输入对象设置 `key` 和 `value`\n+ 输入参数1必须为对象,否则返回 `failure`\n+ 如果 `key` 为 `undefined`, 也返回 `failure`\n+ 如果 `value` 为 `undefined` 或 `null`, 则删除 `key` 的值\n",
},
{ {
name: "Calculate", name: "Calculate",
type: "Action", type: "Action",
@@ -138,6 +110,24 @@ export const zhNodeDef = () => {
output: ["变量"], output: ["变量"],
doc: "+ 只能有一个子节点,多个仅执行第一个\n+ 遍历输入数组,将当前元素写入`变量`\n+ 当子节点返回 `failure` 时,退出遍历并返回 `failure` 状态\n+ 执行完所有子节点后,返回 `success`", doc: "+ 只能有一个子节点,多个仅执行第一个\n+ 遍历输入数组,将当前元素写入`变量`\n+ 当子节点返回 `failure` 时,退出遍历并返回 `failure` 状态\n+ 执行完所有子节点后,返回 `success`",
}, },
{
name: "GetField",
type: "Action",
children: 0,
status: ["success", "failure"],
desc: "获取对象的字段值",
args: [
{
name: "field",
type: "string?",
desc: "字段(field)",
oneof: "字段(field)",
},
],
input: ["对象", "字段(field)?"],
output: ["字段值(value)"],
doc: "+ 合法元素不包括 `undefined` 和 `null`\n+ 只有获取到合法元素时候才会返回 `success`,否则返回 `failure`\n",
},
{ {
name: "IfElse", name: "IfElse",
type: "Composite", type: "Composite",
@@ -160,18 +150,18 @@ export const zhNodeDef = () => {
type: "Action", type: "Action",
children: 0, children: 0,
status: ["success", "failure"], status: ["success", "failure"],
desc: "索引输入的数组或对象", desc: "索引输入的数组",
args: [ args: [
{ {
name: "idx", name: "index",
type: "string?", type: "int?",
desc: "索引", desc: "索引",
oneof: "索引", oneof: "索引",
}, },
], ],
input: ["输入目标", "索引?"], input: ["数组", "索引?"],
output: ["输出目标"], output: ["值"],
doc: "+ 合法元素不包括 `undefined` 和 `null`\n+ 只有索引到有合法元素时候才会返回 `success`,否则返回 `failure`\n", doc: "+ 合法元素不包括 `undefined` 和 `null`\n+ 索引数组的时候,第一个元素的索引为 0\n+ 只有索引到有合法元素时候才会返回 `success`,否则返回 `failure`\n",
}, },
{ {
name: "Invert", name: "Invert",
@@ -198,14 +188,14 @@ export const zhNodeDef = () => {
input: ["已存在变量名?"], input: ["已存在变量名?"],
args: [ args: [
{ {
name: "expr", name: "value",
type: "json?", type: "json?",
desc: "表达式", desc: "值(value)",
oneof: "已存在变量名", oneof: "已存在变量名",
}, },
], ],
output: ["新变量名"], output: ["新变量名"],
doc: "+ 如果有输入变量,则给已有变量重新定义一个名字\n+ 如果有表达式,则使用表达式\n+ 如果表达式为 `null`,则清除变量\n", doc: "+ 如果有输入变量,则给已有变量重新定义一个名字\n+ 如果`值(value)`为 `null`,则清除变量\n",
}, },
{ {
name: "Listen", name: "Listen",
@@ -441,6 +431,29 @@ export const zhNodeDef = () => {
desc: "顺序执行", desc: "顺序执行",
doc: "+ 一直往下执行,只有当所有子节点都返回 `success`, 才返回 `success`\n+ 若子节点返回 `failure`,则直接返回 `failure` 状态\n+ 其余情况返回 `running` 状态\n", doc: "+ 一直往下执行,只有当所有子节点都返回 `success`, 才返回 `success`\n+ 若子节点返回 `failure`,则直接返回 `failure` 状态\n+ 其余情况返回 `running` 状态\n",
}, },
{
name: "SetField",
type: "Action",
children: 0,
status: ["success", "failure"],
desc: "设置对象字段值",
input: ["输入对象", "字段(field)?", "值(value)?"],
args: [
{
name: "field",
type: "string?",
desc: "字段(field)",
oneof: "字段(field)",
},
{
name: "value",
type: "json?",
desc: "值(value)",
oneof: "值(value)",
},
],
doc: "+ 对输入对象设置 `field` 和 `value`\n+ 输入参数1必须为对象,否则返回 `failure`\n+ 如果 `field` 不为 `string`, 也返回 `failure`\n+ 如果 `value` 为 `undefined` 或 `null`, 则删除 `field` 的值\n",
},
{ {
name: "Timeout", name: "Timeout",
type: "Decorator", type: "Decorator",