From 3d5ae730711bfa94664621abb938bbce03635590 Mon Sep 17 00:00:00 2001 From: JeffDotPng Date: Mon, 26 Feb 2024 19:34:01 -0500 Subject: [PATCH] start useFlowchartGraph rewrite --- src/renderer/hooks/useCustomBlockManifest.ts | 6 +- src/renderer/hooks/useFlowChartGraph.ts | 12 -- src/renderer/hooks/useManifest.ts | 4 +- src/renderer/lib/sync.ts | 4 +- src/renderer/services/FlowChartServices.ts | 4 +- src/renderer/stores/project.ts | 212 +++++++++++++++++++ src/renderer/types/blocks-metadata.ts | 2 +- src/renderer/types/node.ts | 7 +- src/renderer/types/project.ts | 8 + src/renderer/types/util.ts | 1 + src/renderer/utils/ManifestLoader.ts | 1 + src/types/result.ts | 8 + 12 files changed, 245 insertions(+), 24 deletions(-) create mode 100644 src/renderer/stores/project.ts create mode 100644 src/renderer/types/project.ts create mode 100644 src/renderer/types/util.ts diff --git a/src/renderer/hooks/useCustomBlockManifest.ts b/src/renderer/hooks/useCustomBlockManifest.ts index 7ce64e9a0..b6c9fe55b 100644 --- a/src/renderer/hooks/useCustomBlockManifest.ts +++ b/src/renderer/hooks/useCustomBlockManifest.ts @@ -1,5 +1,5 @@ import { captain } from "@/renderer/lib/ky"; -import { BlocksMetadataMap } from "@/renderer/types/blocks-metadata"; +import { BlockMetadataMap } from "@/renderer/types/blocks-metadata"; import { RootNode, validateRootSchema } from "@/renderer/utils/ManifestLoader"; import { atom, useAtom, useSetAtom } from "jotai"; import { useCallback } from "react"; @@ -8,7 +8,7 @@ import { manifestChangedAtom } from "./useManifest"; // undefined = loading state const customBlockManifestAtom = atom(null); -const customBlocksMetadataMapAtom = atom( +const customBlocksMetadataMapAtom = atom( null, ); @@ -54,7 +54,7 @@ export const useCustomSections = () => { }, }) .json(); - setCustomBlocksMetadata(res2 as BlocksMetadataMap); + setCustomBlocksMetadata(res2 as BlockMetadataMap); setManifestChanged(true); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { diff --git a/src/renderer/hooks/useFlowChartGraph.ts b/src/renderer/hooks/useFlowChartGraph.ts index 417dbbced..8af331b72 100644 --- a/src/renderer/hooks/useFlowChartGraph.ts +++ b/src/renderer/hooks/useFlowChartGraph.ts @@ -133,17 +133,6 @@ export const useFlowChartGraph = () => { setNodes(updatedNodes); }; - const removeCtrlInputDataForNode = (nodeId: string, paramId: string) => { - setNodes((nodes) => { - const node = nodes.find((e) => e.id === nodeId); - if (node) { - node.data.ctrls = node.data.ctrls || {}; - delete node.data.ctrls[paramId]; - } - }); - sendEventToMix("Control Input Data Removed", { nodeId, paramId }); - }; - const handleNodeChanges = ( cb: (nodes: Node[]) => Node[], ) => { @@ -165,7 +154,6 @@ export const useFlowChartGraph = () => { selectedNode, unSelectedNodes, updateCtrlInputDataForNode, - removeCtrlInputDataForNode, updateInitCtrlInputDataForNode, loadFlowExportObject, handleTitleChange, diff --git a/src/renderer/hooks/useManifest.ts b/src/renderer/hooks/useManifest.ts index 0c465cbb7..4d069bf8a 100644 --- a/src/renderer/hooks/useManifest.ts +++ b/src/renderer/hooks/useManifest.ts @@ -2,7 +2,7 @@ import { getManifest, getBlocksMetadata, } from "@/renderer/services/FlowChartServices"; -import { BlocksMetadataMap } from "@/renderer/types/blocks-metadata"; +import { BlockMetadataMap } from "@/renderer/types/blocks-metadata"; import { RootNode } from "@/renderer/utils/ManifestLoader"; import { atom, useAtomValue, useSetAtom } from "jotai"; import { useCallback, useMemo } from "react"; @@ -24,7 +24,7 @@ export const useFetchManifest = () => { export const useManifest = () => useAtomValue(manifestAtom); -const nodesMetadataMapAtom = atom(null); +const nodesMetadataMapAtom = atom(null); export const useFetchNodesMetadata = () => { const setNodesMetadata = useSetAtom(nodesMetadataMapAtom); diff --git a/src/renderer/lib/sync.ts b/src/renderer/lib/sync.ts index b89c51b08..c30e098e2 100644 --- a/src/renderer/lib/sync.ts +++ b/src/renderer/lib/sync.ts @@ -1,5 +1,5 @@ import { BlockData } from "@/renderer/types"; -import { BlocksMetadataMap } from "@/renderer/types/blocks-metadata"; +import { BlockMetadataMap } from "@/renderer/types/blocks-metadata"; import { Leaf, RootNode, TreeNode } from "@/renderer/utils/ManifestLoader"; import { Edge, Node } from "reactflow"; import { CtrlData } from "@/renderer/types/node"; @@ -11,7 +11,7 @@ export function syncFlowchartWithManifest( nodes: Node[], edges: Edge[], blockManifest: RootNode, - blockMetadata: BlocksMetadataMap, + blockMetadata: BlockMetadataMap, ): [Node[], Edge[]] { const blocks = flattenManifest(blockManifest); diff --git a/src/renderer/services/FlowChartServices.ts b/src/renderer/services/FlowChartServices.ts index 200a49804..1561e5115 100644 --- a/src/renderer/services/FlowChartServices.ts +++ b/src/renderer/services/FlowChartServices.ts @@ -6,7 +6,7 @@ import { captain } from "@/renderer/lib/ky"; import { HTTPError } from "ky"; import { RootNode, validateRootSchema } from "@/renderer/utils/ManifestLoader"; import { toast } from "sonner"; -import { BlocksMetadataMap } from "@/renderer/types/blocks-metadata"; +import { BlockMetadataMap } from "@/renderer/types/blocks-metadata"; import { EnvVar } from "../types/envVar"; export const postEnvironmentVariable = async ( @@ -122,7 +122,7 @@ export const getManifest = async () => { export const getBlocksMetadata = async () => { try { const res = await captain.get("blocks/metadata").json(); - return res as BlocksMetadataMap; + return res as BlockMetadataMap; } catch (err: unknown) { if (err instanceof HTTPError) { toast.message("Failed to generate blocks metadata", { diff --git a/src/renderer/stores/project.ts b/src/renderer/stores/project.ts new file mode 100644 index 000000000..8eb7407d0 --- /dev/null +++ b/src/renderer/stores/project.ts @@ -0,0 +1,212 @@ +import { Node, Edge } from "reactflow"; +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { BlockData } from "../types/"; +import { BlockParameterValue, TextData } from "../types/node"; + +import * as galleryItems from "../data/apps"; +import { ExampleProjects } from "../data/docs-example-apps"; +import * as RECIPES from "../data/RECIPES"; +import { BlockManifest } from "../utils/ManifestLoader"; +import { BlockMetadataMap } from "../types/blocks-metadata"; +import { Project } from "../types/project"; +import { syncFlowchartWithManifest } from "../lib/sync"; +import { sendEventToMix } from "../services/MixpanelServices"; +import { Err, Ok, Result } from "@/types/result"; + +type State = { + name: string | undefined; + nodes: Node[]; // TODO: Turn this into a record for fast lookup + edges: Edge[]; + textNodes: Node[]; +}; + +type Actions = { + loadProject: ( + project: Project, + manifest: BlockManifest, + metadata: BlockMetadataMap, + ) => void; + updateBlockParameter: ( + blockId: string, + paramName: string, + value: BlockParameterValue, + ) => Result; + updateBlockInitParameter: ( + blockId: string, + paramName: string, + value: BlockParameterValue, + ) => Result; + updateBlockName: (blockId: string, name: string) => Result; + + handleNodeChanges: ( + cb: (nodes: Node[]) => Node[], + ) => void; + + handleEdgeChanges: (cb: (nodes: Edge[]) => Edge[]) => void; +}; + +const defaultProjectData = + resolveProjectReference(resolveDefaultProjectReference()) ?? + RECIPES.NOISY_SINE; +const initialNodes: Node[] = defaultProjectData.nodes; +const initialEdges: Edge[] = defaultProjectData.edges; + +export const useProjectStore = create()( + immer((set) => ({ + name: undefined, + nodes: initialNodes, + edges: initialEdges, + textNodes: [], + + handleNodeChanges: ( + cb: (nodes: Node[]) => Node[], + ) => { + set((state) => { + cb(state.nodes); + }); + }, + handleEdgeChanges: (cb: (edges: Edge[]) => Edge[]) => { + set((state) => { + cb(state.edges); + }); + }, + + loadProject: ( + project: Project, + manifest: BlockManifest, + metadata: BlockMetadataMap, + ) => { + const { + rfInstance: { nodes, edges }, + textNodes, + } = project; + const [syncedNodes, syncedEdges] = syncFlowchartWithManifest( + nodes, + edges, + manifest, + metadata, + ); + set({ + nodes: syncedNodes, + edges: syncedEdges, + textNodes: textNodes ?? [], + }); + + // toast("Synced blocks with manifest."); + + sendEventToMix("Flow Export Object Loaded"); + }, + updateBlockParameter: ( + blockId: string, + paramName: string, + value: BlockParameterValue, + ) => { + try { + set((state) => { + const block = state.nodes.find((e) => e.id === blockId); + if (!block) { + return Err(new Error("Block not found")); + } + + block.data.ctrls[paramName].value = value; + if (block.data.func === "CONSTANT" && paramName === "constant") { + block.data.label = value?.toString() ?? "CONSTANT"; + } + }); + } catch (e) { + return Err(e as Error); + } + + sendEventToMix("Control Input Data Updated", { + blockId, + paramName, + value, + }); + + return Ok(undefined); + }, + + updateBlockInitParameter: ( + blockId: string, + paramName: string, + value: BlockParameterValue, + ) => { + try { + set((state) => { + const block = state.nodes.find((e) => e.id === blockId); + if (!block) { + throw new Error("Block not found"); + } + + if (!block.data.initCtrls) { + throw new Error("Block has no init parameters"); + } + + block.data.initCtrls[paramName].value = value; + }); + } catch (e) { + return Err(e as Error); + } + + sendEventToMix("Control Input Data Updated", { + blockId, + paramName, + value, + }); + return Ok(undefined); + }, + + updateBlockName: (blockId: string, name: string) => { + try { + set((state) => { + const node = state.nodes.find((n) => n.data.id === blockId); + if (node === undefined) { + throw new Error("Block not found"); + } + + if (name === node?.data.label) { + return; + } + + const isDuplicate = state.nodes.find( + (n) => n.data.label === name && n.data.id !== blockId, + ); + if (isDuplicate) { + throw new Error( + `There is another node with the same label: ${name}`, + ); + } + node.data.label = name; + }); + } catch (e) { + return Err(e as Error); + } + + sendEventToMix("Block Name Changed", { blockId, name }); + return Ok(undefined); + }, + })), +); + +function resolveDefaultProjectReference() { + if (typeof window !== "undefined") { + const query = new URLSearchParams(window.location.search); + return query.get("project"); // TODO: set these env through electron API as process is not accessible at this level + } + return undefined; +} + +function resolveProjectReference(project: string | null | undefined) { + if (!project) { + return null; + } + + if (RECIPES[project]) { + return RECIPES[project]; + } else if (galleryItems[project]) { + return galleryItems[project].rfInstance; + } else if (ExampleProjects[project]) { + return ExampleProjects[project].rfInstance; + } +} diff --git a/src/renderer/types/blocks-metadata.ts b/src/renderer/types/blocks-metadata.ts index a973a8124..06adcd34d 100644 --- a/src/renderer/types/blocks-metadata.ts +++ b/src/renderer/types/blocks-metadata.ts @@ -1,4 +1,4 @@ -export type BlocksMetadataMap = { +export type BlockMetadataMap = { [node: string]: { metadata: string; path: string; diff --git a/src/renderer/types/node.ts b/src/renderer/types/node.ts index af48b2142..24ce4a246 100644 --- a/src/renderer/types/node.ts +++ b/src/renderer/types/node.ts @@ -1,4 +1,7 @@ import { NodeProps } from "reactflow"; +import { Nullish } from "./util"; + +export type BlockParameterValue = Nullish; type BlockDefinition = { name: string; @@ -22,7 +25,7 @@ type BlockDefinition = { string, { type: string; - default?: string | number | boolean | null | undefined; + default?: BlockParameterValue; options?: Array; desc: string | null; overload: Record | null; @@ -41,7 +44,7 @@ export type CtrlData = Record< ? U & { functionName: string; param: string; - value: string | boolean | number | undefined | null; + value: BlockParameterValue; } : never : never diff --git a/src/renderer/types/project.ts b/src/renderer/types/project.ts new file mode 100644 index 000000000..1369202b6 --- /dev/null +++ b/src/renderer/types/project.ts @@ -0,0 +1,8 @@ +import { ReactFlowJsonObject, Node } from "reactflow"; +import { TextData, BlockData } from "./node"; + +export type Project = { + name?: string; + rfInstance: ReactFlowJsonObject; + textNodes?: Node[]; +}; diff --git a/src/renderer/types/util.ts b/src/renderer/types/util.ts new file mode 100644 index 000000000..0ed401931 --- /dev/null +++ b/src/renderer/types/util.ts @@ -0,0 +1 @@ +export type Nullish = T | null | undefined; diff --git a/src/renderer/utils/ManifestLoader.ts b/src/renderer/utils/ManifestLoader.ts index fc3b448d9..38dcc403c 100644 --- a/src/renderer/utils/ManifestLoader.ts +++ b/src/renderer/utils/ManifestLoader.ts @@ -61,6 +61,7 @@ const rootSchema = z.object({ }); export type RootNode = z.infer; +export type BlockManifest = RootNode; export type RootChild = z.infer["children"][0]; export const validateRootSchema = (schema: RootNode) => { diff --git a/src/types/result.ts b/src/types/result.ts index 5135e76a2..222812782 100644 --- a/src/types/result.ts +++ b/src/types/result.ts @@ -8,3 +8,11 @@ export function Ok(value: T): Result { export function Err(error: E): Result { return { ok: false, error }; } + +export function tryCatch(fn: () => T): Result { + try { + return Ok(fn()); + } catch (e) { + return Err(e as Error); + } +}