Jon/sky 5820 make browser task block flippable with code (#3165)

This commit is contained in:
Jonathan Dobson
2025-08-11 19:57:08 -04:00
committed by GitHub
parent e5106124e3
commit 039fce0bb3
16 changed files with 663 additions and 339 deletions

View File

@@ -0,0 +1,100 @@
import { ExitIcon } from "@radix-ui/react-icons";
import { Handle } from "@xyflow/react";
import { Position } from "@xyflow/react";
import { WorkflowBlockType } from "@/routes/workflows/types/workflowTypes";
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
import { cn } from "@/util/utils";
import { CodeEditor } from "./CodeEditor";
import { workflowBlockTitle } from "../editor/nodes/types";
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
function BlockCodeEditor({
blockLabel,
blockType,
script,
onClick,
}: {
blockLabel: string;
blockType: WorkflowBlockType;
script: string | undefined;
onClick?: (e: React.MouseEvent) => void;
}) {
const blockTitle = workflowBlockTitle[blockType];
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
return (
<div className="h-full">
<Handle
type="source"
position={Position.Bottom}
id="a"
className="opacity-0"
/>
<Handle
type="target"
position={Position.Top}
id="b"
className="opacity-0"
/>
<div
className={cn(
"transform-origin-center flex h-full w-[30rem] flex-col space-y-4 rounded-lg border border-slate-600 bg-slate-elevation3 px-6 py-4 transition-all",
)}
onClick={(e) => {
onClick?.(e);
}}
>
<header className="!mt-0 flex h-[2.75rem] justify-between gap-2">
<div className="flex w-full gap-2">
<div className="relative flex h-[2.75rem] w-[2.75rem] items-center justify-center overflow-hidden rounded border border-slate-600">
<WorkflowBlockIcon
workflowBlockType={blockType}
className="size-6"
/>
<div className="absolute -left-3 top-8 flex h-4 w-16 origin-top-left -rotate-45 transform items-center justify-center bg-yellow-400">
<span className="text-xs font-bold text-black">code</span>
</div>
</div>
<div className="flex flex-col gap-1">
{blockLabel}
<span className="text-xs text-slate-400">{blockTitle}</span>
</div>
<div className="ml-auto flex w-[2.75rem] items-center justify-center rounded hover:bg-slate-800">
<ExitIcon
onClick={() => {
toggleScriptForNodeCallback({
label: blockLabel,
show: false,
});
}}
className="size-5 cursor-pointer"
/>
</div>
</div>
</header>
{script ? (
<div className="h-full flex-1 overflow-y-hidden">
<CodeEditor
key="static"
className="nopan nowheel h-full overflow-y-scroll"
language="python"
value={script}
lineWrap={false}
readOnly
fontSize={10}
/>
</div>
) : (
<div className="flex h-full w-full items-center justify-center bg-slate-950">
No script defined
</div>
)}
</div>
</div>
);
}
export { BlockCodeEditor };

View File

@@ -20,6 +20,7 @@ type Props = {
value: string; value: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
language?: "python" | "json" | "html"; language?: "python" | "json" | "html";
lineWrap?: boolean;
readOnly?: boolean; readOnly?: boolean;
minHeight?: string; minHeight?: string;
maxHeight?: string; maxHeight?: string;
@@ -33,13 +34,14 @@ function CodeEditor({
minHeight, minHeight,
maxHeight, maxHeight,
language, language,
lineWrap = true,
className, className,
readOnly = false, readOnly = false,
fontSize = 12, fontSize = 12,
}: Props) { }: Props) {
const extensions = language const extensions = language
? [getLanguageExtension(language), EditorView.lineWrapping] ? [getLanguageExtension(language), lineWrap ? EditorView.lineWrapping : []]
: [EditorView.lineWrapping]; : [lineWrap ? EditorView.lineWrapping : []];
return ( return (
<CodeMirror <CodeMirror

View File

@@ -10,7 +10,7 @@ import {
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { useOnChange } from "@/hooks/useOnChange"; import { useOnChange } from "@/hooks/useOnChange";
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab"; import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext"; import { BlockActionContext } from "@/store/BlockActionContext";
import { useDebugStore } from "@/store/useDebugStore"; import { useDebugStore } from "@/store/useDebugStore";
import { import {
useWorkflowHasChangesStore, useWorkflowHasChangesStore,
@@ -543,6 +543,37 @@ function FlowRenderer({
doLayout(newNodesWithUpdatedParameters, newEdges); doLayout(newNodesWithUpdatedParameters, newEdges);
} }
function toggleScript({
id,
label,
show,
}: {
id?: string;
label?: string;
show: boolean;
}) {
if (id) {
const node = nodes.find((node) => node.id === id);
if (!node || !isWorkflowBlockNode(node)) {
return;
}
node.data.showCode = show;
} else if (label) {
const node = nodes.find(
(node) => "label" in node.data && node.data.label === label,
);
if (!node || !isWorkflowBlockNode(node)) {
return;
}
node.data.showCode = show;
}
doLayout(nodes, edges);
}
const editorElementRef = useRef<HTMLDivElement>(null); const editorElementRef = useRef<HTMLDivElement>(null);
useAutoPan(editorElementRef, nodes); useAutoPan(editorElementRef, nodes);
@@ -642,7 +673,12 @@ function FlowRenderer({
<WorkflowParametersStateContext.Provider <WorkflowParametersStateContext.Provider
value={[parameters, setParameters]} value={[parameters, setParameters]}
> >
<DeleteNodeCallbackContext.Provider value={deleteNode}> <BlockActionContext.Provider
value={{
deleteNodeCallback: deleteNode,
toggleScriptForNodeCallback: toggleScript,
}}
>
<ReactFlow <ReactFlow
ref={editorElementRef} ref={editorElementRef}
nodes={nodes} nodes={nodes}
@@ -772,7 +808,7 @@ function FlowRenderer({
</Panel> </Panel>
)} )}
</ReactFlow> </ReactFlow>
</DeleteNodeCallbackContext.Provider> </BlockActionContext.Provider>
</WorkflowParametersStateContext.Provider> </WorkflowParametersStateContext.Provider>
</> </>
); );

View File

@@ -24,6 +24,8 @@ import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useMountEffect } from "@/hooks/useMountEffect"; import { useMountEffect } from "@/hooks/useMountEffect";
import { statusIsFinalized } from "@/routes/tasks/types.ts"; import { statusIsFinalized } from "@/routes/tasks/types.ts";
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { useSidebarStore } from "@/store/SidebarStore"; import { useSidebarStore } from "@/store/SidebarStore";
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
@@ -47,12 +49,18 @@ function WorkflowDebugger() {
const credentialGetter = useCredentialGetter(); const credentialGetter = useCredentialGetter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false); const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
const blockScriptStore = useBlockScriptStore();
const { data: workflowRun } = useWorkflowRunQuery(); const { data: workflowRun } = useWorkflowRunQuery();
const { data: workflow } = useWorkflowQuery({ const { data: workflow } = useWorkflowQuery({
workflowPermanentId, workflowPermanentId,
}); });
const { data: blockScripts } = useBlockScriptsQuery({
workflowPermanentId,
});
const { data: debugSession } = useDebugSessionQuery({ const { data: debugSession } = useDebugSessionQuery({
workflowPermanentId, workflowPermanentId,
enabled: shouldFetchDebugSession && !!workflowPermanentId, enabled: shouldFetchDebugSession && !!workflowPermanentId,
@@ -80,6 +88,11 @@ function WorkflowDebugger() {
} }
}); });
useEffect(() => {
blockScriptStore.setScripts(blockScripts ?? {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blockScripts]);
const afterCycleBrowser = () => { const afterCycleBrowser = () => {
setOpenDialogue(false); setOpenDialogue(false);
setShowPowerButton(false); setShowPowerButton(false);

View File

@@ -1,4 +1,7 @@
import { useEffect } from "react";
import { useMountEffect } from "@/hooks/useMountEffect"; import { useMountEffect } from "@/hooks/useMountEffect";
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { useSidebarStore } from "@/store/SidebarStore"; import { useSidebarStore } from "@/store/SidebarStore";
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
import { ReactFlowProvider } from "@xyflow/react"; import { ReactFlowProvider } from "@xyflow/react";
@@ -17,6 +20,7 @@ function WorkflowEditor() {
return state.setCollapsed; return state.setCollapsed;
}); });
const workflowChangesStore = useWorkflowHasChangesStore(); const workflowChangesStore = useWorkflowHasChangesStore();
const blockScriptStore = useBlockScriptStore();
const { data: workflow, isLoading } = useWorkflowQuery({ const { data: workflow, isLoading } = useWorkflowQuery({
workflowPermanentId, workflowPermanentId,
@@ -25,11 +29,20 @@ function WorkflowEditor() {
const { data: globalWorkflows, isLoading: isGlobalWorkflowsLoading } = const { data: globalWorkflows, isLoading: isGlobalWorkflowsLoading } =
useGlobalWorkflowsQuery(); useGlobalWorkflowsQuery();
const { data: blockScripts } = useBlockScriptsQuery({
workflowPermanentId,
});
useMountEffect(() => { useMountEffect(() => {
setCollapsed(true); setCollapsed(true);
workflowChangesStore.setHasChanges(false); workflowChangesStore.setHasChanges(false);
}); });
useEffect(() => {
blockScriptStore.setScripts(blockScripts ?? {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blockScripts]);
if (isLoading || isGlobalWorkflowsLoading) { if (isLoading || isGlobalWorkflowsLoading) {
return ( return (
<div className="flex h-screen w-full items-center justify-center"> <div className="flex h-screen w-full items-center justify-center">

View File

@@ -1,3 +1,5 @@
import { useEffect } from "react";
import { Flippable } from "@/components/Flippable";
import { HelpTooltip } from "@/components/HelpTooltip"; import { HelpTooltip } from "@/components/HelpTooltip";
import { import {
Accordion, Accordion,
@@ -12,7 +14,9 @@ import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { import {
Handle, Handle,
NodeProps, NodeProps,
@@ -40,7 +44,10 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
const { blockLabel: urlBlockLabel } = useParams(); const { blockLabel: urlBlockLabel } = useParams();
const debugStore = useDebugStore(); const debugStore = useDebugStore();
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const { editable, debuggable, label } = data; const { editable, debuggable, label } = data;
const script = blockScriptStore.scripts[label];
const thisBlockIsPlaying = const thisBlockIsPlaying =
urlBlockLabel !== undefined && urlBlockLabel === label; urlBlockLabel !== undefined && urlBlockLabel === label;
const elideFromDebugging = debugStore.isDebugMode && !debuggable; const elideFromDebugging = debugStore.isDebugMode && !debuggable;
@@ -76,7 +83,12 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
}, [data.showCode]);
return ( return (
<Flippable facing={facing} preserveFrontsideHeight={true}>
<div> <div>
<Handle <Handle
type="source" type="source"
@@ -90,6 +102,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
id="b" id="b"
className="opacity-0" className="opacity-0"
/> />
<div <div
className={cn( className={cn(
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all", "transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
@@ -139,7 +152,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<Label className="text-xs text-slate-300">Prompt</Label> <Label className="text-xs text-slate-300">
Navigation Goal
</Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["navigation"]["navigationGoal"]} content={helpTooltips["navigation"]["navigationGoal"]}
/> />
@@ -233,7 +248,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
</div> </div>
<Input <Input
type="number" type="number"
placeholder={placeholders["navigation"]["maxStepsOverride"]} placeholder={
placeholders["navigation"]["maxStepsOverride"]
}
className="nopan w-52 text-xs" className="nopan w-52 text-xs"
min="0" min="0"
value={inputs.maxStepsOverride ?? ""} value={inputs.maxStepsOverride ?? ""}
@@ -253,7 +270,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
Error Messages Error Messages
</Label> </Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["navigation"]["errorCodeMapping"]} content={
helpTooltips["navigation"]["errorCodeMapping"]
}
/> />
</div> </div>
<Checkbox <Checkbox
@@ -263,7 +282,11 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
handleChange( handleChange(
"errorCodeMapping", "errorCodeMapping",
checked checked
? JSON.stringify(errorMappingExampleValue, null, 2) ? JSON.stringify(
errorMappingExampleValue,
null,
2,
)
: "null", : "null",
); );
}} }}
@@ -315,7 +338,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
Continue on Failure Continue on Failure
</Label> </Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["navigation"]["continueOnFailure"]} content={
helpTooltips["navigation"]["continueOnFailure"]
}
/> />
</div> </div>
<div className="w-52"> <div className="w-52">
@@ -352,7 +377,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
Complete on Download Complete on Download
</Label> </Label>
<HelpTooltip <HelpTooltip
content={helpTooltips["navigation"]["completeOnDownload"]} content={
helpTooltips["navigation"]["completeOnDownload"]
}
/> />
</div> </div>
<div className="w-52"> <div className="w-52">
@@ -429,6 +456,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
</Accordion> </Accordion>
</div> </div>
</div> </div>
<BlockCodeEditor blockLabel={label} blockType={type} script={script} />
</Flippable>
); );
} }

View File

@@ -7,12 +7,14 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { DotsHorizontalIcon } from "@radix-ui/react-icons"; import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { OrgWalled } from "@/components/Orgwalled";
type Props = { type Props = {
onDelete: () => void; onDelete: () => void;
onShowScript?: () => void;
}; };
function NodeActionMenu({ onDelete }: Props) { function NodeActionMenu({ onDelete, onShowScript }: Props) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -28,6 +30,17 @@ function NodeActionMenu({ onDelete }: Props) {
> >
Delete Block Delete Block
</DropdownMenuItem> </DropdownMenuItem>
<OrgWalled className="p-0">
{onShowScript && (
<DropdownMenuItem
onSelect={() => {
onShowScript();
}}
>
Show Script
</DropdownMenuItem>
)}
</OrgWalled>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );

View File

@@ -12,6 +12,7 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQuery"; import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQuery";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { import {
@@ -145,6 +146,7 @@ function NodeHeader({
}); });
const blockTitle = workflowBlockTitle[type]; const blockTitle = workflowBlockTitle[type];
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
const credentialGetter = useCredentialGetter(); const credentialGetter = useCredentialGetter();
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -411,6 +413,9 @@ function NodeHeader({
onDelete={() => { onDelete={() => {
deleteNodeCallback(nodeId); deleteNodeCallback(nodeId);
}} }}
onShowScript={() =>
toggleScriptForNodeCallback({ id: nodeId, show: true })
}
/> />
</div> </div>
</div> </div>

View File

@@ -7,6 +7,7 @@ export type NodeBaseData = {
continueOnFailure: boolean; continueOnFailure: boolean;
editable: boolean; editable: boolean;
model: WorkflowModel | null; model: WorkflowModel | null;
showCode?: boolean;
}; };
export const errorMappingExampleValue = { export const errorMappingExampleValue = {

View File

@@ -0,0 +1,37 @@
import { getClient } from "@/api/AxiosClient";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useQuery } from "@tanstack/react-query";
import { ScriptBlocksResponse } from "../types/scriptTypes";
type Props = {
cacheKey?: string;
cacheKeyValue?: string;
workflowPermanentId?: string;
};
function useBlockScriptsQuery({
cacheKey,
cacheKeyValue,
workflowPermanentId,
}: Props) {
const credentialGetter = useCredentialGetter();
return useQuery<{ [blockName: string]: string }>({
queryKey: ["block-scripts", workflowPermanentId, cacheKey, cacheKeyValue],
queryFn: async () => {
const client = await getClient(credentialGetter, "sans-api-v1");
const result = await client
.post<ScriptBlocksResponse>(`/scripts/${workflowPermanentId}/blocks`, {
cache_key: cacheKey ?? "",
cache_key_value: cacheKeyValue ?? "",
})
.then((response) => response.data);
return result.blocks;
},
enabled: !!workflowPermanentId,
});
}
export { useBlockScriptsQuery };

View File

@@ -1,12 +1,12 @@
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext"; import { BlockActionContext } from "@/store/BlockActionContext";
import { useContext } from "react"; import { useContext } from "react";
function useDeleteNodeCallback() { function useDeleteNodeCallback() {
const deleteNodeCallback = useContext(DeleteNodeCallbackContext); const deleteNodeCallback = useContext(BlockActionContext)?.deleteNodeCallback;
if (!deleteNodeCallback) { if (!deleteNodeCallback) {
throw new Error( throw new Error(
"useDeleteNodeCallback must be used within a DeleteNodeCallbackProvider", "useDeleteNodeCallback must be used within a BlockActionContextProvider",
); );
} }

View File

@@ -0,0 +1,17 @@
import { BlockActionContext } from "@/store/BlockActionContext";
import { useContext } from "react";
function useToggleScriptForNodeCallback() {
const toggleScriptForNodeCallback =
useContext(BlockActionContext)?.toggleScriptForNodeCallback;
if (!toggleScriptForNodeCallback) {
throw new Error(
"useToggleScriptForNodeCallback must be used within a BlockActionContextProvider",
);
}
return toggleScriptForNodeCallback;
}
export { useToggleScriptForNodeCallback };

View File

@@ -0,0 +1,3 @@
export type ScriptBlocksResponse = {
blocks: { [blockName: string]: string };
};

View File

@@ -0,0 +1,18 @@
import { createContext } from "react";
type DeleteNodeCallback = (id: string) => void;
type ToggleScriptForNodeCallback = (opts: {
id?: string;
label?: string;
show: boolean;
}) => void;
const BlockActionContext = createContext<
| {
deleteNodeCallback: DeleteNodeCallback;
toggleScriptForNodeCallback?: ToggleScriptForNodeCallback;
}
| undefined
>(undefined);
export { BlockActionContext };

View File

@@ -0,0 +1,45 @@
/**
* A store to hold the scripts for individual blocks in a workflow. As each
* workflow has uniquely (and differently) labelled blocks, and those labels
* are block identity, we'll eschew strong typing for this, and use a loose
* object literal instead.
*/
import { create } from "zustand";
interface BlockScriptStore {
scriptId?: string;
scripts: { [k: string]: string };
// --
setScript: (blockId: string, script: string) => void;
setScripts: (scripts: { [k: string]: string }) => void;
reset: () => void;
}
const useBlockScriptStore = create<BlockScriptStore>((set) => {
return {
scriptId: undefined,
scripts: {},
// --
setScript: (blockId: string, script: string) => {
set((state) => ({
scripts: {
...state.scripts,
[blockId]: script,
},
}));
},
setScripts: (scripts: { [k: string]: string }) => {
set(() => ({
scripts,
}));
},
reset: () => {
set({
scripts: {},
});
},
};
});
export { useBlockScriptStore };

View File

@@ -1,9 +0,0 @@
import { createContext } from "react";
type DeleteNodeCallback = (id: string) => void;
const DeleteNodeCallbackContext = createContext<DeleteNodeCallback | undefined>(
undefined,
);
export { DeleteNodeCallbackContext };