Feat: Add option to replace saved component or create a new one using confirmation modal

This commit is contained in:
igorrCarvalho 2023-11-23 22:06:07 -03:00
commit 7e4dd16881
15 changed files with 659 additions and 556 deletions

View file

@ -22,7 +22,10 @@ import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent"; import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent"; import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
import { LANGFLOW_SUPPORTED_TYPES, TOOLTIP_EMPTY } from "../../../../constants/constants"; import {
LANGFLOW_SUPPORTED_TYPES,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { FlowsContext } from "../../../../contexts/flowsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import { ParameterComponentType } from "../../../../types/components"; import { ParameterComponentType } from "../../../../types/components";
@ -222,9 +225,7 @@ export default function ParameterComponent({
}, [tooltipTitle, flow]); }, [tooltipTitle, flow]);
return !showNode ? ( return !showNode ? (
left && left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
(LANGFLOW_SUPPORTED_TYPES.has(type??"")) &&
!optionalHandle ? (
<></> <></>
) : ( ) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted"> <Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
@ -297,9 +298,7 @@ export default function ParameterComponent({
)} )}
</div> </div>
</div> </div>
{left && {left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
(LANGFLOW_SUPPORTED_TYPES.has(type??"")) &&
!optionalHandle ? (
<></> <></>
) : ( ) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted"> <Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">

File diff suppressed because it is too large Load diff

View file

@ -664,5 +664,14 @@ export const LAST_USED_SPAN_1 = "The last time this key was used.";
export const LAST_USED_SPAN_2 = export const LAST_USED_SPAN_2 =
"Accurate to within the hour from the most recent usage."; "Accurate to within the hour from the most recent usage.";
export const LANGFLOW_SUPPORTED_TYPES= new Set(["str","bool","float","code", export const LANGFLOW_SUPPORTED_TYPES = new Set([
"prompt","file","int","dict","NestedDict"]) "str",
"bool",
"float",
"code",
"prompt",
"file",
"int",
"dict",
"NestedDict",
]);

View file

@ -56,7 +56,11 @@ const FlowsContextInitialValue: FlowsContextType = {
isLoading: true, isLoading: true,
flows: [], flows: [],
removeFlow: (id: string) => {}, removeFlow: (id: string) => {},
addFlow: async (newProject: boolean, flowData?: FlowType) => "", addFlow: async (
newProject: boolean,
flowData?: FlowType,
override?: boolean
) => "",
updateFlow: (newFlow: FlowType) => {}, updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uid(), incrementNodeId: () => uid(),
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
@ -78,7 +82,7 @@ const FlowsContextInitialValue: FlowsContextType = {
selection: { nodes: any; edges: any }, selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number } position: { x: number; y: number; paneX?: number; paneY?: number }
) => {}, ) => {},
saveComponent: async (component: NodeDataType) => "", saveComponent: async (component: NodeDataType, override: boolean) => "",
deleteComponent: (key: string) => {}, deleteComponent: (key: string) => {},
version: "", version: "",
}; };
@ -496,7 +500,8 @@ export function FlowsProvider({ children }: { children: ReactNode }) {
const addFlow = async ( const addFlow = async (
newProject: Boolean, newProject: Boolean,
flow?: FlowType flow?: FlowType,
override?: boolean
): Promise<String | undefined> => { ): Promise<String | undefined> => {
if (newProject) { if (newProject) {
let flowData = flow let flowData = flow
@ -505,6 +510,15 @@ export function FlowsProvider({ children }: { children: ReactNode }) {
// Create a new flow with a default name if no flow is provided. // Create a new flow with a default name if no flow is provided.
if (override) {
deleteComponent(flow!.name);
const newFlow = createNewFlow(flowData, flow!);
const { id } = await saveFlowToDatabase(newFlow);
newFlow.id = id;
addFlowToLocalState(newFlow);
return;
}
const newFlow = createNewFlow(flowData, flow!); const newFlow = createNewFlow(flowData, flow!);
const newName = addVersionToDuplicates(newFlow, flows); const newName = addVersionToDuplicates(newFlow, flows);
@ -642,9 +656,9 @@ export function FlowsProvider({ children }: { children: ReactNode }) {
} }
} }
function saveComponent(component: NodeDataType) { function saveComponent(component: NodeDataType, override: boolean) {
component.node!.official = false; component.node!.official = false;
return addFlow(true, createFlowComponent(component, version)); return addFlow(true, createFlowComponent(component, version), override);
} }
function deleteComponent(key: string) { function deleteComponent(key: string) {

View file

@ -13,7 +13,10 @@ import {
// import "ace-builds/webpack-resolver"; // import "ace-builds/webpack-resolver";
import CodeTabsComponent from "../../components/codeTabsComponent"; import CodeTabsComponent from "../../components/codeTabsComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { EXPORT_CODE_DIALOG, LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants"; import {
EXPORT_CODE_DIALOG,
LANGFLOW_SUPPORTED_TYPES,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { FlowsContext } from "../../contexts/flowsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { TemplateVariableType } from "../../types/api"; import { TemplateVariableType } from "../../types/api";
@ -99,7 +102,9 @@ const ApiModal = forwardRef(
(templateField) => (templateField) =>
templateField.charAt(0) !== "_" && templateField.charAt(0) !== "_" &&
node.data.node.template[templateField].show && node.data.node.template[templateField].show &&
(LANGFLOW_SUPPORTED_TYPES.has(node.data.node.template[templateField].type)) LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField].type
)
) )
.map((n, i) => { .map((n, i) => {
arrNodesWithValues.push(node["id"]); arrNodesWithValues.push(node["id"]);

View file

@ -36,10 +36,15 @@ function ConfirmationModal({
size, size,
open, open,
onClose, onClose,
onCancel,
}: ConfirmationModalType) { }: ConfirmationModalType) {
const Icon: any = nodeIconsLucide[icon]; const Icon: any = nodeIconsLucide[icon];
const [modalOpen, setModalOpen] = useState(open ?? false); const [modalOpen, setModalOpen] = useState(open ?? false);
useEffect(() => {
if (open) setModalOpen(open);
}, [open]);
useEffect(() => { useEffect(() => {
if (onClose) onClose!(modalOpen); if (onClose) onClose!(modalOpen);
}, [modalOpen]); }, [modalOpen]);
@ -86,6 +91,7 @@ function ConfirmationModal({
className="mt-5" className="mt-5"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
if (onCancel) onCancel();
setModalOpen(false); setModalOpen(false);
}} }}
> >

View file

@ -25,7 +25,10 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import { LANGFLOW_SUPPORTED_TYPES, limitScrollFieldsModal } from "../../constants/constants"; import {
LANGFLOW_SUPPORTED_TYPES,
limitScrollFieldsModal,
} from "../../constants/constants";
import { FlowsContext } from "../../contexts/flowsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
@ -145,7 +148,9 @@ const EditNodeModal = forwardRef(
(templateParam) => (templateParam) =>
templateParam.charAt(0) !== "_" && templateParam.charAt(0) !== "_" &&
myData.current.node?.template[templateParam].show && myData.current.node?.template[templateParam].show &&
(LANGFLOW_SUPPORTED_TYPES.has(myData.current.node.template[templateParam].type)) LANGFLOW_SUPPORTED_TYPES.has(
myData.current.node.template[templateParam].type
)
) )
.map((templateParam, index) => ( .map((templateParam, index) => (
<TableRow key={index} className="h-10"> <TableRow key={index} className="h-10">

View file

@ -6,6 +6,7 @@ import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox"; import { Checkbox } from "../../components/ui/checkbox";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext";
import { import {
getStoreComponents, getStoreComponents,
getStoreTags, getStoreTags,
@ -15,7 +16,6 @@ import { FlowType } from "../../types/flow";
import { removeApiKeys } from "../../utils/reactflowUtils"; import { removeApiKeys } from "../../utils/reactflowUtils";
import { getTagsIds } from "../../utils/storeUtils"; import { getTagsIds } from "../../utils/storeUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
import { StoreContext } from "../../contexts/storeContext";
export default function ShareModal({ export default function ShareModal({
component, component,
@ -23,7 +23,7 @@ export default function ShareModal({
children, children,
open, open,
setOpen, setOpen,
disabled disabled,
}: { }: {
children?: ReactNode; children?: ReactNode;
is_component: boolean; is_component: boolean;
@ -33,7 +33,7 @@ export default function ShareModal({
disabled?: boolean; disabled?: boolean;
}): JSX.Element { }): JSX.Element {
const { version, addFlow } = useContext(FlowsContext); const { version, addFlow } = useContext(FlowsContext);
const {hasApiKey} = useContext(StoreContext) const { hasApiKey } = useContext(StoreContext);
const { setSuccessData, setErrorData } = useContext(alertContext); const { setSuccessData, setErrorData } = useContext(alertContext);
const [checked, setChecked] = useState(true); const [checked, setChecked] = useState(true);
const [name, setName] = useState(component?.name ?? ""); const [name, setName] = useState(component?.name ?? "");
@ -50,12 +50,12 @@ export default function ShareModal({
useEffect(() => { useEffect(() => {
if (open || internalOpen) { if (open || internalOpen) {
if(hasApiKey){ if (hasApiKey) {
handleGetTags(); handleGetTags();
handleGetNames(); handleGetNames();
} }
} }
}, [open, internalOpen,hasApiKey]); }, [open, internalOpen, hasApiKey]);
function handleGetTags() { function handleGetTags() {
setLoadingTags(true); setLoadingTags(true);

View file

@ -324,7 +324,7 @@ export default function Page({
// Calculate the position where the node should be created // Calculate the position where the node should be created
const position = reactFlowInstance!.screenToFlowPosition({ const position = reactFlowInstance!.screenToFlowPosition({
x: event.clientX, x: event.clientX,
y: event.clientY y: event.clientY,
}); });
// Generate a unique node ID // Generate a unique node ID

View file

@ -6,6 +6,7 @@ import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator"; import { Separator } from "../../../../components/ui/separator";
import { alertContext } from "../../../../contexts/alertContext"; import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { StoreContext } from "../../../../contexts/storeContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import ApiModal from "../../../../modals/ApiModal"; import ApiModal from "../../../../modals/ApiModal";
import ExportModal from "../../../../modals/exportModal"; import ExportModal from "../../../../modals/exportModal";
@ -23,14 +24,13 @@ import {
} from "../../../../utils/utils"; } from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent"; import DisclosureComponent from "../DisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent"; import SidebarDraggableComponent from "./sideBarDraggableComponent";
import { StoreContext } from "../../../../contexts/storeContext";
export default function ExtraSidebar(): JSX.Element { export default function ExtraSidebar(): JSX.Element {
const { data, templates, getFilterEdge, setFilterEdge, reactFlowInstance } = const { data, templates, getFilterEdge, setFilterEdge, reactFlowInstance } =
useContext(typesContext); useContext(typesContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } = const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } =
useContext(FlowsContext); useContext(FlowsContext);
const {hasApiKey} = useContext(StoreContext) const { hasApiKey } = useContext(StoreContext);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data); const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@ -50,8 +50,6 @@ export default function ExtraSidebar(): JSX.Element {
event.dataTransfer.setData("nodedata", JSON.stringify(data)); event.dataTransfer.setData("nodedata", JSON.stringify(data));
} }
// Handle showing components after use search input // Handle showing components after use search input
function handleSearchInput(e: string) { function handleSearchInput(e: string) {
if (e === "") { if (e === "") {
@ -199,7 +197,10 @@ export default function ExtraSidebar(): JSX.Element {
() => ( () => (
<ExportModal> <ExportModal>
<ShadTooltip content="Export" side="top"> <ShadTooltip content="Export" side="top">
<button disabled={!hasApiKey} className={classNames("extra-side-bar-buttons")}> <button
disabled={!hasApiKey}
className={classNames("extra-side-bar-buttons")}
>
<IconComponent name="FileDown" className="side-bar-button-size" /> <IconComponent name="FileDown" className="side-bar-button-size" />
</button> </button>
</ShadTooltip> </ShadTooltip>

View file

@ -93,12 +93,11 @@ export default function SidebarDraggableComponent({
> >
<span className="side-bar-components-text">{display_name}</span> <span className="side-bar-components-text">{display_name}</span>
<div> <div>
<IconComponent <IconComponent
name="Menu" name="Menu"
className="side-bar-components-icon " className="side-bar-components-icon "
/> />
<SelectTrigger> <SelectTrigger></SelectTrigger>
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={"download"}> <SelectItem value={"download"}>
<div className="flex"> <div className="flex">

View file

@ -10,6 +10,8 @@ import {
SelectTrigger, SelectTrigger,
} from "../../../../components/ui/select-custom"; } from "../../../../components/ui/select-custom";
import { FlowsContext } from "../../../../contexts/flowsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { StoreContext } from "../../../../contexts/storeContext";
import ConfirmationModal from "../../../../modals/ConfirmationModal";
import EditNodeModal from "../../../../modals/EditNodeModal"; import EditNodeModal from "../../../../modals/EditNodeModal";
import ShareModal from "../../../../modals/shareModal"; import ShareModal from "../../../../modals/shareModal";
import { nodeToolbarPropsType } from "../../../../types/components"; import { nodeToolbarPropsType } from "../../../../types/components";
@ -21,7 +23,6 @@ import {
updateFlowPosition, updateFlowPosition,
} from "../../../../utils/reactflowUtils"; } from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils"; import { classNames } from "../../../../utils/utils";
import { StoreContext } from "../../../../contexts/storeContext";
export default function NodeToolbarComponent({ export default function NodeToolbarComponent({
data, data,
@ -51,7 +52,7 @@ export default function NodeToolbarComponent({
); );
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const { getNodeId } = useContext(FlowsContext); const { getNodeId } = useContext(FlowsContext);
const {hasApiKey} = useContext(StoreContext) const { hasApiKey } = useContext(StoreContext);
function canMinimize() { function canMinimize() {
let countHandles: number = 0; let countHandles: number = 0;
@ -64,11 +65,12 @@ export default function NodeToolbarComponent({
const isMinimal = canMinimize(); const isMinimal = canMinimize();
const isGroup = data.node?.flow ? true : false; const isGroup = data.node?.flow ? true : false;
const { paste, saveComponent, version } = useContext(FlowsContext); const { paste, saveComponent, version, flows } = useContext(FlowsContext);
const reactFlowInstance = useReactFlow(); const reactFlowInstance = useReactFlow();
const [showModalAdvanced, setShowModalAdvanced] = useState(false); const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false); const [showconfirmShare, setShowconfirmShare] = useState(false);
const [selectedValue, setSelectedValue] = useState(""); const [selectedValue, setSelectedValue] = useState("");
const [showOverrideModal, setShowOverrideModal] = useState(false);
const [flowComponent, setFlowComponent] = useState<FlowType>(); const [flowComponent, setFlowComponent] = useState<FlowType>();
@ -76,6 +78,11 @@ export default function NodeToolbarComponent({
setFlowComponent(createFlowComponent(cloneDeep(data), version)); setFlowComponent(createFlowComponent(cloneDeep(data), version));
}, [data]); }, [data]);
function onOverrideModalConfirmation(override: boolean): void {
if (override) saveComponent(cloneDeep(data), true);
saveComponent(cloneDeep(data), false);
}
const handleSelectChange = (event) => { const handleSelectChange = (event) => {
switch (event) { switch (event) {
case "advanced": case "advanced":
@ -89,10 +96,10 @@ export default function NodeToolbarComponent({
downloadNode(createFlowComponent(cloneDeep(data), version)); downloadNode(createFlowComponent(cloneDeep(data), version));
break; break;
case "Share": case "Share":
if(hasApiKey) setShowconfirmShare(true); if (hasApiKey) setShowconfirmShare(true);
break; break;
case "SaveAll": case "SaveAll":
saveComponent(cloneDeep(data)); saveComponent(cloneDeep(data), false);
break; break;
case "disabled": case "disabled":
break; break;
@ -100,9 +107,16 @@ export default function NodeToolbarComponent({
updateFlowPosition(position, data.node?.flow!); updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance, getNodeId); expandGroupNode(data, reactFlowInstance, getNodeId);
break; break;
case "override":
setShowOverrideModal(true);
break;
} }
}; };
const isSaved = flows.some((flow) =>
Object.values(flow).includes(data.node?.display_name!)
);
return ( return (
<> <>
<div className="w-26 h-10"> <div className="w-26 h-10">
@ -204,24 +218,38 @@ export default function NodeToolbarComponent({
</SelectItem> </SelectItem>
)} )}
<SelectItem value={"SaveAll"}> {isSaved ? (
<div className="flex"> <SelectItem value={"override"}>
<IconComponent <div className="flex">
name="SaveAll" <IconComponent
className="relative top-0.5 mr-2 h-4 w-4" name="SaveAll"
/>{" "} className="relative top-0.5 mr-2 h-4 w-4"
Save{" "} />{" "}
</div>{" "} Save{" "}
</SelectItem> </div>{" "}
{hasApiKey && <SelectItem value={"Share"}> </SelectItem>
<div className="flex"> ) : (
<IconComponent <SelectItem value={"SaveAll"}>
name="Share2" <div className="flex">
className="relative top-0.5 mr-2 h-4 w-4" <IconComponent
/>{" "} name="SaveAll"
Share{" "} className="relative top-0.5 mr-2 h-4 w-4"
</div>{" "} />{" "}
</SelectItem>} Save{" "}
</div>{" "}
</SelectItem>
)}
{hasApiKey && (
<SelectItem value={"Share"}>
<div className="flex">
<IconComponent
name="Share2"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Share{" "}
</div>{" "}
</SelectItem>
)}
<SelectItem value={"Download"}> <SelectItem value={"Download"}>
<div className="flex"> <div className="flex">
<IconComponent <IconComponent
@ -255,6 +283,32 @@ export default function NodeToolbarComponent({
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
<ConfirmationModal
asChild
open={showOverrideModal}
title={`Replace ${data.node?.display_name}`}
titleHeader={`Please, confirm your save actions`}
modalContentTitle="Attention!"
cancelText="New"
confirmationText="Replace"
icon={"SaveAll"}
index={6}
onConfirm={(index, user) => {
saveComponent(cloneDeep(data), true);
}}
onClose={setShowOverrideModal}
onCancel={() => saveComponent(cloneDeep(data), false)}
>
<ConfirmationModal.Content>
<span>
It seems {data.node?.display_name} already exists. Replacing it
will switch the current component. Proceed with replacement?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
</ConfirmationModal.Trigger>
</ConfirmationModal>
<EditNodeModal <EditNodeModal
data={data} data={data}
setData={setData} setData={setData}

View file

@ -280,6 +280,7 @@ export type PaginatorComponentType = {
}; };
export type ConfirmationModalType = { export type ConfirmationModalType = {
onCancel?: () => void;
title: string; title: string;
titleHeader: string; titleHeader: string;
asChild?: boolean; asChild?: boolean;

View file

@ -10,7 +10,8 @@ export type FlowsContextType = {
removeFlow: (id: string) => void; removeFlow: (id: string) => void;
addFlow: ( addFlow: (
newProject: boolean, newProject: boolean,
flow?: FlowType flow?: FlowType,
override?: boolean
) => Promise<String | undefined>; ) => Promise<String | undefined>;
updateFlow: (newFlow: FlowType) => void; updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => string; incrementNodeId: () => string;
@ -44,7 +45,10 @@ export type FlowsContextType = {
setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void; setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void;
setTweak: (tweak: tweakType) => tweakType | void; setTweak: (tweak: tweakType) => tweakType | void;
getTweak: tweakType; getTweak: tweakType;
saveComponent: (component: NodeDataType) => Promise<String | undefined>; saveComponent: (
component: NodeDataType,
override: boolean
) => Promise<String | undefined>;
deleteComponent: (key: string) => void; deleteComponent: (key: string) => void;
version: string; version: string;
}; };

View file

@ -9,7 +9,10 @@ import {
XYPosition, XYPosition,
} from "reactflow"; } from "reactflow";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { LANGFLOW_SUPPORTED_TYPES, specialCharsRegex } from "../constants/constants"; import {
LANGFLOW_SUPPORTED_TYPES,
specialCharsRegex,
} from "../constants/constants";
import { APITemplateType, TemplateVariableType } from "../types/api"; import { APITemplateType, TemplateVariableType } from "../types/api";
import { import {
FlowType, FlowType,