add node def inspector

This commit is contained in:
codetypes committed 2024-04-20 00:35:33 +08:00
1 parent f8d04581d1
commit 47c058a831
4 files changed
+365 -300

No files matched your search

+9 -2
View File
@@ -105,8 +105,6 @@ const renameFile = (oldPath: string, newPath: string) => {
return false; return false;
}; };
const getSvgIcon = (path: string) => {};
export const Explorer: FC = () => { export const Explorer: FC = () => {
const workspace = { const workspace = {
close: useWorkspace((state) => state.close), close: useWorkspace((state) => state.close),
@@ -116,6 +114,7 @@ export const Explorer: FC = () => {
nodeDefs: useWorkspace((state) => state.nodeDefs), nodeDefs: useWorkspace((state) => state.nodeDefs),
nodeTree: useWorkspace((state) => state.nodeTree), nodeTree: useWorkspace((state) => state.nodeTree),
workdir: useWorkspace((state) => state.workdir), workdir: useWorkspace((state) => state.workdir),
onEditingNodeDef: useWorkspace((state) => state.onEditingNodeDef),
open: useWorkspace((state) => state.open), open: useWorkspace((state) => state.open),
}; };
const { t } = useTranslation(); const { t } = useTranslation();
@@ -798,6 +797,14 @@ export const Explorer: FC = () => {
fieldNames={{ key: "title" }} fieldNames={{ key: "title" }}
treeData={workspace.nodeTree ? [workspace.nodeTree] : []} treeData={workspace.nodeTree ? [workspace.nodeTree] : []}
draggable={{ icon: false, nodeDraggable: (node) => !!node.isLeaf }} draggable={{ icon: false, nodeDraggable: (node) => !!node.isLeaf }}
onSelect={(_, info) => {
const node = info.node;
if (node && node.isLeaf) {
workspace.onEditingNodeDef({
data: node.def!,
});
}
}}
onDragStart={(e) => { onDragStart={(e) => {
e.event.dataTransfer.setData("explore-node", e.node.def?.name ?? ""); e.event.dataTransfer.setData("explore-node", e.node.def?.name ?? "");
}} }}
+340 -283
View File
@@ -10,11 +10,60 @@ import Markdown from "react-markdown";
interface OptionType extends DefaultOptionType {} interface OptionType extends DefaultOptionType {}
export const Inspector: FC = () => { const TreeInspector: FC = () => {
const workspace = { const workspace = {
editing: useWorkspace((state) => state.editing), editing: useWorkspace((state) => state.editing),
editingNode: useWorkspace((state) => state.editingNode), editingTree: useWorkspace((state) => state.editingTree)!,
editingTree: useWorkspace((state) => state.editingTree), };
const { t } = useTranslation();
const [form] = Form.useForm();
// set form values
useEffect(() => {
const data = workspace.editingTree.data;
form.resetFields();
form.setFieldValue("name", data.name);
form.setFieldValue("desc", data.desc);
}, [workspace.editingTree]);
const finish = (values: any) => {
const data = {} as TreeModel;
data.name = values.name;
data.desc = values.desc || undefined;
workspace.editing?.dispatch?.("updateTree", {
data: {
name: values.name,
desc: values.desc || undefined,
},
} as EditTree);
};
return (
<>
<div style={{ padding: "12px 24px" }}>
<span style={{ fontSize: "18px", fontWeight: "600" }}>{t("tree.overview")}</span>
</div>
<div
className={isMacos ? undefined : "b3-overflow"}
style={{ overflow: "auto", height: "100%", padding: "24px" }}
>
<Form form={form} labelCol={{ span: 8 }} onFinish={finish}>
<Form.Item name="name" label={t("tree.name")}>
<Input disabled={true} />
</Form.Item>
<Form.Item name="desc" label={t("tree.desc")}>
<Input onBlur={form.submit} />
</Form.Item>
</Form>
</div>
</>
);
};
const NodeInspector: FC = () => {
const workspace = {
editing: useWorkspace((state) => state.editing),
editingNode: useWorkspace((state) => state.editingNode)!,
getNodeDef: useWorkspace((state) => state.getNodeDef), getNodeDef: useWorkspace((state) => state.getNodeDef),
nodeDefs: useWorkspace((state) => state.nodeDefs), nodeDefs: useWorkspace((state) => state.nodeDefs),
onEditingNode: useWorkspace((state) => state.onEditingNode), onEditingNode: useWorkspace((state) => state.onEditingNode),
@@ -26,31 +75,24 @@ export const Inspector: FC = () => {
// set form values // set form values
useEffect(() => { useEffect(() => {
if (workspace.editingTree) { const data = workspace.editingNode.data;
const data = workspace.editingTree.data; const def = workspace.getNodeDef(workspace.editingNode.data.name);
form.resetFields(); form.resetFields();
form.setFieldValue("name", data.name); form.setFieldValue("id", data.id);
form.setFieldValue("desc", data.desc); form.setFieldValue("name", data.name);
} else if (workspace.editingNode) { form.setFieldValue("desc", data.desc);
const data = workspace.editingNode.data; form.setFieldValue("debug", data.debug);
const def = workspace.getNodeDef(workspace.editingNode.data.name); form.setFieldValue("path", data.path);
form.resetFields(); def.args?.forEach((v) => {
form.setFieldValue("id", data.id); form.setFieldValue(`args.${v.name}`, data.args?.[v.name]);
form.setFieldValue("name", data.name); });
form.setFieldValue("desc", data.desc); def.input?.forEach((_, i) => {
form.setFieldValue("debug", data.debug); form.setFieldValue(`input.${i}`, data.input?.[i]);
form.setFieldValue("path", data.path); });
def.args?.forEach((v) => { def.output?.forEach((_, i) => {
form.setFieldValue(`args.${v.name}`, data.args?.[v.name]); form.setFieldValue(`output.${i}`, data.output?.[i]);
}); });
def.input?.forEach((_, i) => { }, [workspace.editingNode]);
form.setFieldValue(`input.${i}`, data.input?.[i]);
});
def.output?.forEach((_, i) => {
form.setFieldValue(`output.${i}`, data.output?.[i]);
});
}
}, [workspace.editingNode, workspace.editingTree]);
// auto complete for node // auto complete for node
const nodeOptions = useMemo(() => { const nodeOptions = useMemo(() => {
@@ -99,264 +141,279 @@ export const Inspector: FC = () => {
return options; return options;
}, [workspace.allFiles, workspace.fileTree]); }, [workspace.allFiles, workspace.fileTree]);
if (workspace.editingTree) { const editingNode = workspace.editingNode;
const finish = (values: any) => { const def = workspace.getNodeDef(editingNode.data.name);
const data = {} as TreeModel; const disabled = !editingNode.editable;
data.name = values.name;
data.desc = values.desc || undefined; // update value
workspace.editing?.dispatch?.("updateTree", { const finish = (values: any) => {
const data = {} as NodeModel;
data.id = editingNode.data.id;
data.name = values.name;
data.debug = values.debug || undefined;
data.desc = values.desc || undefined;
data.path = values.path || undefined;
def.args?.forEach((arg) => {
const v = values[`args.${arg.name}`];
if (v !== null && v !== undefined && v !== "") {
data.args ||= {};
data.args[arg.name] = v;
}
});
def.input?.forEach((_, i) => {
const v = values[`input.${i}`];
data.input ||= [];
data.input.push(v ?? "");
});
def.output?.forEach((_, i) => {
const v = values[`output.${i}`];
data.output ||= [];
data.output.push(v ?? "");
});
workspace.editing?.dispatch?.("updateNode", {
data: data,
} as EditNode);
};
// change node def
const changeNodeDef = (newname: string) => {
if (editingNode.data.name !== newname) {
workspace.onEditingNode({
data: { data: {
name: values.name, id: editingNode.data.id,
desc: values.desc || undefined, name: workspace.nodeDefs.get(newname)?.name ?? newname,
desc: editingNode.data.desc,
debug: editingNode.data.debug,
}, },
} as EditTree); editable: editingNode.editable,
}; });
return ( finish(form.getFieldsValue());
<Flex className="b3-inspector" vertical style={{ height: "100%" }}> } else {
<div style={{ padding: "12px 24px" }}> form.submit();
<span style={{ fontSize: "18px", fontWeight: "600" }}>{t("tree.overview")}</span> }
</div> };
<div
className={isMacos ? undefined : "b3-overflow"} const changeSubtree = () => {
style={{ overflow: "auto", height: "100%", padding: "24px" }} if (form.getFieldValue("path") !== editingNode.data.path) {
finish(form.getFieldsValue());
} else {
form.submit();
}
};
return (
<>
<div style={{ padding: "12px 24px" }}>
<span style={{ fontSize: "18px", fontWeight: "600" }}>{def.desc}</span>
</div>
<div
className={isMacos ? undefined : "b3-overflow"}
style={{ overflow: "auto", height: "100%", padding: "24px" }}
>
<Form
form={form}
wrapperCol={{ span: "auto" }}
labelCol={{ span: "auto" }}
onFinish={finish}
> >
<Form form={form} labelCol={{ span: 8 }} onFinish={finish}> <Form.Item
<Form.Item name="name" label={t("tree.name")}> name="id"
<Input disabled={true} /> label={
</Form.Item> <div style={{ minWidth: "80px", justifyContent: "flex-end" }}>{t("node.id")}</div>
<Form.Item name="desc" label={t("tree.desc")}> }
<Input onBlur={form.submit} />
</Form.Item>
</Form>
</div>
</Flex>
);
} else if (workspace.editingNode) {
const editingNode = workspace.editingNode;
const def = workspace.getNodeDef(editingNode.data.name);
const disabled = !editingNode.editable;
// update value
const finish = (values: any) => {
const data = {} as NodeModel;
data.id = editingNode.data.id;
data.name = values.name;
data.debug = values.debug || undefined;
data.desc = values.desc || undefined;
data.path = values.path || undefined;
def.args?.forEach((arg) => {
const v = values[`args.${arg.name}`];
if (v !== null && v !== undefined && v !== "") {
data.args ||= {};
data.args[arg.name] = v;
}
});
def.input?.forEach((_, i) => {
const v = values[`input.${i}`];
data.input ||= [];
data.input.push(v ?? "");
});
def.output?.forEach((_, i) => {
const v = values[`output.${i}`];
data.output ||= [];
data.output.push(v ?? "");
});
workspace.editing?.dispatch?.("updateNode", {
data: data,
} as EditNode);
};
// change node def
const changeNodeDef = (newname: string) => {
if (editingNode.data.name !== newname) {
workspace.onEditingNode({
data: {
id: editingNode.data.id,
name: workspace.nodeDefs.get(newname)?.name ?? newname,
desc: editingNode.data.desc,
debug: editingNode.data.debug,
},
editable: editingNode.editable,
});
finish(form.getFieldsValue());
} else {
form.submit();
}
};
const changeSubtree = () => {
if (form.getFieldValue("path") !== editingNode.data.path) {
finish(form.getFieldsValue());
} else {
form.submit();
}
};
return (
<Flex className="b3-inspector" vertical style={{ height: "100%" }}>
<div style={{ padding: "12px 24px" }}>
<span style={{ fontSize: "18px", fontWeight: "600" }}>{def.desc}</span>
</div>
<div
className={isMacos ? undefined : "b3-overflow"}
style={{ overflow: "auto", height: "100%", padding: "24px" }}
>
<Form
form={form}
wrapperCol={{ span: "auto" }}
labelCol={{ span: "auto" }}
onFinish={finish}
> >
<Form.Item <Input disabled={true} />
name="id" </Form.Item>
label={ <Form.Item label={t("node.name")} name="name">
<div style={{ minWidth: "80px", justifyContent: "flex-end" }}>{t("node.id")}</div> <AutoComplete
} disabled={disabled}
> options={nodeOptions}
<Input disabled={true} /> onBlur={() => changeNodeDef(form.getFieldValue("name"))}
</Form.Item> onSelect={changeNodeDef}
<Form.Item label={t("node.name")} name="name"> onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
<AutoComplete filterOption={(inputValue: string, option?: OptionType) => {
disabled={disabled} const label = option!.label as string;
options={nodeOptions} return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
onBlur={() => changeNodeDef(form.getFieldValue("name"))} }}
onSelect={changeNodeDef} />
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()} </Form.Item>
filterOption={(inputValue: string, option?: OptionType) => { <Form.Item name="desc" label={t("node.desc")}>
const label = option!.label as string; <Input disabled={disabled} onBlur={form.submit} />
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1; </Form.Item>
}} <Form.Item label={t("node.debug")} name="debug" valuePropName="checked">
/> <Switch disabled={disabled && !editingNode.data.path} onChange={form.submit} />
</Form.Item> </Form.Item>
<Form.Item name="desc" label={t("node.desc")}> <Form.Item label={t("node.subtree")} name="path">
<Input disabled={disabled} onBlur={form.submit} /> <AutoComplete
</Form.Item> disabled={disabled && !editingNode.data.path}
<Form.Item label={t("node.debug")} name="debug" valuePropName="checked"> options={subtreeOptions}
<Switch disabled={disabled && !editingNode.data.path} onChange={form.submit} /> onBlur={changeSubtree}
</Form.Item> onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
<Form.Item label={t("node.subtree")} name="path"> filterOption={(inputValue: string, option?: OptionType) => {
<AutoComplete const label = option!.label as string;
disabled={disabled && !editingNode.data.path} return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
options={subtreeOptions} }}
onBlur={changeSubtree} />
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()} </Form.Item>
filterOption={(inputValue: string, option?: OptionType) => { <Markdown>{def.doc}</Markdown>
const label = option!.label as string; {def.input && def.input.length > 0 && (
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1; <>
}} <Divider orientation="left">
/> <h4>{t("node.inputVariable")}</h4>
</Form.Item> </Divider>
<Markdown>{def.doc}</Markdown> {def.input.map((v, i) => {
{def.input && def.input.length > 0 && ( const required = v.indexOf("?") == -1;
<> const desc = v.replace("?", "");
<Divider orientation="left"> return (
<h4>{t("node.inputVariable")}</h4> <Form.Item
</Divider> label={desc}
{def.input.map((v, i) => { name={`input.${i}`}
const required = v.indexOf("?") == -1; key={`input.${i}`}
const desc = v.replace("?", ""); rules={[{ required, message: t("node.fileRequired", { field: desc }) }]}
return ( >
<Form.Item <AutoComplete
label={desc} disabled={disabled}
name={`input.${i}`} options={inoutVarOptions}
key={`input.${i}`} onBlur={form.submit}
rules={[{ required, message: t("node.fileRequired", { field: desc }) }]} onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
> filterOption={(inputValue: string, option?: OptionType) => {
<AutoComplete const label = option!.label as string;
disabled={disabled} return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
options={inoutVarOptions} }}
onBlur={form.submit} />
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()} </Form.Item>
filterOption={(inputValue: string, option?: OptionType) => { );
const label = option!.label as string; })}
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1; </>
}} )}
/> {def.args && def.args.length > 0 && (
</Form.Item> <>
); <Divider orientation="left">
})} <h4>{t("node.args")}</h4>
</> </Divider>
)} {def.args.map((v) => {
{def.args && def.args.length > 0 && ( const required = v.type.indexOf("?") == -1;
<> const type = v.type.replace("?", "") as NodeArgType;
<Divider orientation="left"> return (
<h4>{t("node.args")}</h4> <Form.Item
</Divider> name={`args.${v.name}`}
{def.args.map((v) => { label={v.desc}
const required = v.type.indexOf("?") == -1; key={`args.${v.name}`}
const type = v.type.replace("?", "") as NodeArgType; initialValue={type === "boolean" ? v.default ?? false : v.default}
return ( valuePropName={type === "boolean" ? "checked" : undefined}
<Form.Item rules={[{ required, message: t("node.fileRequired", { field: v.desc }) }]}
name={`args.${v.name}`} >
label={v.desc} {type === "string" && <Input disabled={disabled} onBlur={form.submit} />}
key={`args.${v.name}`} {type === "int" && (
initialValue={type === "boolean" ? v.default ?? false : v.default} <InputNumber disabled={disabled} onBlur={form.submit} precision={0} />
valuePropName={type === "boolean" ? "checked" : undefined} )}
rules={[{ required, message: t("node.fileRequired", { field: v.desc }) }]} {type === "float" && <InputNumber disabled={disabled} onBlur={form.submit} />}
> {type === "boolean" && <Switch disabled={disabled} onChange={form.submit} />}
{type === "string" && <Input disabled={disabled} onBlur={form.submit} />} {type === "code" && (
{type === "int" && ( <Input onBlur={form.submit} placeholder={t("node.codePlaceholder")} />
<InputNumber disabled={disabled} onBlur={form.submit} precision={0} /> )}
)} {type === "enum" && (
{type === "float" && <InputNumber disabled={disabled} onBlur={form.submit} />} <Select disabled={disabled} onBlur={form.submit} onChange={form.submit}>
{type === "boolean" && <Switch disabled={disabled} onChange={form.submit} />} {v.options?.map((value) => {
{type === "code" && ( return (
<Input onBlur={form.submit} placeholder={t("node.codePlaceholder")} /> <Select.Option key={value.name} value={value.value}>
)} {value.name}
{type === "enum" && ( </Select.Option>
<Select disabled={disabled} onBlur={form.submit} onChange={form.submit}> );
{v.options?.map((value) => { })}
return ( </Select>
<Select.Option key={value.name} value={value.value}> )}
{value.name} </Form.Item>
</Select.Option> );
); })}
})} </>
</Select> )}
)} {def.output && def.output.length > 0 && (
</Form.Item> <>
); <Divider orientation="left">
})} <h4>{t("node.outputVariable")}</h4>
</> </Divider>
)} {def.output.map((v, i) => {
{def.output && def.output.length > 0 && ( const required = v.indexOf("?") == -1;
<> const desc = v.replace("?", "");
<Divider orientation="left"> return (
<h4>{t("node.outputVariable")}</h4> <Form.Item
</Divider> label={desc}
{def.output.map((v, i) => { name={`output.${i}`}
const required = v.indexOf("?") == -1; key={`output.${i}`}
const desc = v.replace("?", ""); rules={[{ required, message: t("node.fileRequired", { field: desc }) }]}
return ( >
<Form.Item <AutoComplete
label={desc} disabled={disabled}
name={`output.${i}`} options={inoutVarOptions}
key={`output.${i}`} onBlur={form.submit}
rules={[{ required, message: t("node.fileRequired", { field: desc }) }]} onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
> filterOption={(inputValue: string, option?: OptionType) => {
<AutoComplete const label = option!.label as string;
disabled={disabled} return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
options={inoutVarOptions} }}
onBlur={form.submit} />
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()} </Form.Item>
filterOption={(inputValue: string, option?: OptionType) => { );
const label = option!.label as string; })}
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1; </>
}} )}
/> </Form>
</Form.Item> </div>
); </>
})} );
</> };
)}
</Form> const NodeDefInspector: FC = () => {
</div> const [form] = Form.useForm();
</Flex> return (
); <>
} else { <div style={{ padding: "12px 24px" }}>
return null; <span style={{ fontSize: "18px", fontWeight: "600" }}>Node Definition</span>
</div>
<div
className={isMacos ? undefined : "b3-overflow"}
style={{ overflow: "auto", height: "100%", padding: "24px" }}
>
<Form
form={form}
wrapperCol={{ span: "auto" }}
labelCol={{ span: "auto" }}
// onFinish={finish}
></Form>
</div>
</>
);
};
export const Inspector: FC = () => {
const workspace = {
editingNode: useWorkspace((state) => state.editingNode),
editingTree: useWorkspace((state) => state.editingTree),
editingNodeDef: useWorkspace((state) => state.editingNodeDef),
};
let isEditingNode = false;
let isEditingTree = false;
let isEditingNodeDef = false;
if (workspace.editingNodeDef) {
isEditingNodeDef = true;
} else if (workspace.editingTree) {
isEditingTree = true;
} else if (workspace.editingNode) {
isEditingNode = true;
} }
return (
<Flex
vertical
className="b3-inspector"
style={{ height: "100%", width: "340px", borderLeft: `1px solid var(--b3-color-border)` }}
>
{isEditingNodeDef && <NodeDefInspector />}
{isEditingTree && <TreeInspector />}
{isEditingNode && <NodeInspector />}
</Flex>
);
}; };
+1 -12
View File
@@ -409,18 +409,7 @@ export const Workspace: FC = () => {
/> />
)} )}
</Content> </Content>
{workspace.editors.length > 0 && ( <Inspector />
<Sider
className="b3-inspector-wrapper"
width={340}
style={{
height: "100%",
borderLeft: `1px solid var(--b3-color-border)`,
}}
>
<Inspector />
</Sider>
)}
</Layout> </Layout>
</Layout> </Layout>
); );
+15 -3
View File
@@ -93,6 +93,10 @@ export type EditNode = {
editable: boolean; editable: boolean;
}; };
export type EditNodeDef = {
data: NodeDef;
};
export type EditTree = { export type EditTree = {
data: TreeModel; data: TreeModel;
}; };
@@ -133,6 +137,10 @@ export type WorkspaceStore = {
editingNode?: EditNode | null; editingNode?: EditNode | null;
onEditingNode: (node: EditNode) => void; onEditingNode: (node: EditNode) => void;
// edit node def
editingNodeDef?: EditNodeDef | null;
onEditingNodeDef: (node: EditNodeDef) => void;
// edit tree // edit tree
editingTree?: EditTree | null; editingTree?: EditTree | null;
onEditingTree: (tree: EditTree | null) => void; onEditingTree: (tree: EditTree | null) => void;
@@ -373,7 +381,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
} }
if (editors.length === 0) { if (editors.length === 0) {
editting = undefined; editting = undefined;
set({ editingNode: undefined }); set({ editingNode: undefined, editingTree: undefined });
} }
set({ editing: editting, editors: editors }); set({ editing: editting, editors: editors });
}, },
@@ -479,12 +487,16 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
// node edit // node edit
onEditingNode: (node) => { onEditingNode: (node) => {
set({ editingNode: node }); set({ editingNode: node, editingNodeDef: null });
},
onEditingNodeDef: (nodeDef) => {
set({ editingNodeDef: nodeDef });
}, },
// tree edit // tree edit
onEditingTree: (tree) => { onEditingTree: (tree) => {
set({ editingTree: tree }); set({ editingTree: tree, editingNodeDef: null });
}, },
// node def // node def