feat: add breaking change update modal, refactor dismissed updates (#7882)
* fix: add optional method property to OutputFieldType * feat: Enhance GenericNode with breaking change detection - Added state management for breaking changes in GenericNode. - Updated useCheckCodeValidity hook to evaluate breaking changes based on outputs and template keys. - Improved node status color logic to reflect breaking changes and outdated code. - Enhanced UI feedback for users with appropriate alerts and dismiss options. * refactor: Improve breaking change handling in useCheckCodeValidity hook - Simplified logic for detecting breaking changes and outdated code. - Updated state management to ensure accurate status updates based on user inputs and templates. - Enhanced readability by consolidating related checks into a single conditional structure. * Fix outdated check * Componentized breaking change * Updated design of update handle on node * Added small-update to modal sizes * updated duplicate flow hook to duplicate just a flow * Added update component modal with updating for single component * Added new duplicateFlow on dropdown on main page * use new update code modal on generic node * delete check code validity * add new check code vaildity util function * removed unused sets from update node code * Make componentsToUpdate contain breaking info * Make Generic Node use Components to Update * Change border in Node Status * Stop propagation on node update * Update update all components to have changes from figma * updated flow store type and added components to update * Update update component modal * added icon on outdatedNodes * Added id filtering on update components * Added table with components to update * Update styling * Update update component modal to use table component * Updated styles * filter map * Update select to not allow selecting texts on backup flow * Update cursor for label * Update text of backup flow * Try to update selection * Fix selection of components on opening modal * Insert Update button on node toolbar if dismissed * Added new parameters of node toolbar * Added new types of node toolbar * Removed update button from node status * Updated shadcn theme * Added dismiss by node, added dismissing to local storage, added correct update display * Clarified update warnings in the UpdateComponentModal to better inform users about potential breaking changes and the need to reconnect components. * Refactored update component visibility logic in GenericNode to use a memoized value for improved performance and readability. * Updated test for outdated components to reflect changes in button selectors and improved visibility assertions for update notifications. * Simplified visibility assertion in outdated components test to check for a more concise update message. * Fixed edges not coming back after undoing * Fixed breaking change check to not be checked if code is the same * Fixed imports * removed unused functions * updated icon color * updated test id * updated for function to foreach * updated data testid * updated outdated flow * removed flowToCanvas that caused bug when going from main page to flow page * [autofix.ci] apply automated fixes * Fixed outdated actions test * fixed timeouts * Added check for Backup --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
3d8e7f3dff
commit
79e35834b5
30 changed files with 1160 additions and 419 deletions
|
|
@ -39,10 +39,11 @@ export default function NodeStatus({
|
|||
showNode,
|
||||
data,
|
||||
buildStatus,
|
||||
dismissAll,
|
||||
isOutdated,
|
||||
isUserEdited,
|
||||
isBreakingChange,
|
||||
getValidationStatus,
|
||||
handleUpdateComponent,
|
||||
}: {
|
||||
nodeId: string;
|
||||
display_name: string;
|
||||
|
|
@ -52,10 +53,11 @@ export default function NodeStatus({
|
|||
showNode: boolean;
|
||||
data: NodeDataType;
|
||||
buildStatus: BuildStatus;
|
||||
dismissAll: boolean;
|
||||
isOutdated: boolean;
|
||||
isUserEdited: boolean;
|
||||
isBreakingChange: boolean;
|
||||
getValidationStatus: (data) => VertexBuildTypeAPI | null;
|
||||
handleUpdateComponent: () => void;
|
||||
}) {
|
||||
const nodeId_ = data.node?.flow?.data
|
||||
? (findLastNode(data.node?.flow.data!)?.id ?? nodeId)
|
||||
|
|
@ -182,8 +184,6 @@ export default function NodeStatus({
|
|||
getValidationStatus,
|
||||
);
|
||||
|
||||
const dismissAll = useUtilityStore((state) => state.dismissAll);
|
||||
|
||||
const getBaseBorderClass = (selected) => {
|
||||
let className =
|
||||
selected && !isBuilding
|
||||
|
|
@ -191,8 +191,8 @@ export default function NodeStatus({
|
|||
: "border ring-[0.5px] hover:shadow-node ring-border";
|
||||
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
|
||||
let updateClass =
|
||||
isOutdated && !isUserEdited && !dismissAll
|
||||
? "border-warning ring-2 ring-warning"
|
||||
isOutdated && !isUserEdited && !dismissAll && isBreakingChange
|
||||
? "border-warning"
|
||||
: "";
|
||||
return cn(frozen ? frozenClass : className, updateClass);
|
||||
};
|
||||
|
|
@ -464,36 +464,6 @@ export default function NodeStatus({
|
|||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
{dismissAll && isOutdated && !isUserEdited && (
|
||||
<ShadTooltip content="Update component">
|
||||
<div
|
||||
className="button-run-bg hit-area-icon ml-1 bg-warning hover:bg-warning/80"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUpdateComponent();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{showNode && (
|
||||
<Button
|
||||
unstyled
|
||||
type="button"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
data-testid={`button_update_` + display_name.toLowerCase()}
|
||||
>
|
||||
<IconComponent
|
||||
name={"AlertTriangle"}
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
className="icon-size text-black"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
||||
import { cn } from "@/utils/utils";
|
||||
|
||||
export default function NodeUpdateComponent({
|
||||
hasBreakingChange,
|
||||
showNode,
|
||||
handleUpdateCode,
|
||||
loadingUpdate,
|
||||
setDismissAll,
|
||||
}: {
|
||||
hasBreakingChange: boolean;
|
||||
showNode: boolean;
|
||||
handleUpdateCode: () => void;
|
||||
loadingUpdate: boolean;
|
||||
setDismissAll: (value: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-t-[0.69rem] border-b bg-muted p-2 px-4 py-2",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-2.5 w-2.5 rounded-full",
|
||||
hasBreakingChange ? "bg-warning" : "bg-status-green",
|
||||
)}
|
||||
/>
|
||||
<div className="mb-px flex-1 truncate text-mmd font-medium">
|
||||
{showNode && (hasBreakingChange ? "Update available" : "Update ready")}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 !text-mmd"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDismissAll(true);
|
||||
}}
|
||||
aria-label="Dismiss warning bar"
|
||||
data-testid="dismiss-warning-bar"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="!h-8 shrink-0 !text-mmd"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUpdateCode();
|
||||
}}
|
||||
loading={loadingUpdate}
|
||||
data-testid={hasBreakingChange ? "review-button" : "update-button"}
|
||||
>
|
||||
{hasBreakingChange ? "Review" : "Update"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
||||
import UpdateComponentModal from "@/modals/updateComponentModal";
|
||||
import { useAlternate } from "@/shared/hooks/use-alternate";
|
||||
import { useUtilityStore } from "@/stores/utilityStore";
|
||||
import { FlowStoreType } from "@/types/zustand/flow";
|
||||
import { useUpdateNodeInternals } from "@xyflow/react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
ICON_STROKE_WIDTH,
|
||||
|
|
@ -24,12 +26,12 @@ import { NodeDataType } from "../../types/flow";
|
|||
import { checkHasToolMode } from "../../utils/reactflowUtils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields";
|
||||
import useCheckCodeValidity from "../hooks/use-check-code-validity";
|
||||
import useUpdateNodeCode from "../hooks/use-update-node-code";
|
||||
import NodeDescription from "./components/NodeDescription";
|
||||
import NodeName from "./components/NodeName";
|
||||
import { OutputParameter } from "./components/NodeOutputParameter";
|
||||
import NodeStatus from "./components/NodeStatus";
|
||||
import NodeUpdateComponent from "./components/NodeUpdateComponent";
|
||||
import RenderInputParameters from "./components/RenderInputParameters";
|
||||
import { NodeIcon } from "./components/nodeIcon";
|
||||
import { useBuildStatus } from "./hooks/use-get-build-status";
|
||||
|
|
@ -72,13 +74,12 @@ function GenericNode({
|
|||
xPos?: number;
|
||||
yPos?: number;
|
||||
}): JSX.Element {
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const [isUserEdited, setIsUserEdited] = useState(false);
|
||||
const [borderColor, setBorderColor] = useState<string>("");
|
||||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
||||
const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<VertexBuildTypeAPI | null>(null);
|
||||
const [openUpdateModal, setOpenUpdateModal] = useState(false);
|
||||
|
||||
const types = useTypesStore((state) => state.types);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
|
|
@ -90,7 +91,15 @@ function GenericNode({
|
|||
const edges = useFlowStore((state) => state.edges);
|
||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||
const buildStatus = useBuildStatus(data, data.id);
|
||||
const dismissAll = useUtilityStore((state) => state.dismissAll);
|
||||
const dismissedNodes = useFlowStore((state) => state.dismissedNodes);
|
||||
const addDismissedNodes = useFlowStore((state) => state.addDismissedNodes);
|
||||
const removeDismissedNodes = useFlowStore(
|
||||
(state) => state.removeDismissedNodes,
|
||||
);
|
||||
const dismissAll = useMemo(
|
||||
() => dismissedNodes.includes(data.id),
|
||||
[dismissedNodes, data.id],
|
||||
);
|
||||
|
||||
const showNode = data.showNode ?? true;
|
||||
|
||||
|
|
@ -104,16 +113,31 @@ function GenericNode({
|
|||
const [editNameDescription, toggleEditNameDescription, set] =
|
||||
useAlternate(false);
|
||||
|
||||
const componentUpdate = useFlowStore(
|
||||
useShallow((state: FlowStoreType) =>
|
||||
state.componentsToUpdate.find((component) => component.id === data.id),
|
||||
),
|
||||
);
|
||||
const {
|
||||
outdated: isOutdated,
|
||||
breakingChange: hasBreakingChange,
|
||||
userEdited: isUserEdited,
|
||||
} = componentUpdate ?? {
|
||||
outdated: false,
|
||||
breakingChange: false,
|
||||
userEdited: false,
|
||||
};
|
||||
|
||||
const updateNodeCode = useUpdateNodeCode(
|
||||
data?.id,
|
||||
data.node!,
|
||||
setNode,
|
||||
setIsOutdated,
|
||||
setIsUserEdited,
|
||||
updateNodeInternals,
|
||||
);
|
||||
|
||||
useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
|
||||
useEffect(() => {
|
||||
updateNodeInternals(data.id);
|
||||
}, [data.node.template]);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
|
|
@ -127,52 +151,61 @@ function GenericNode({
|
|||
deleteNode(data.id);
|
||||
}
|
||||
|
||||
const handleUpdateCode = useCallback(() => {
|
||||
setLoadingUpdate(true);
|
||||
takeSnapshot();
|
||||
const handleUpdateCode = useCallback(
|
||||
(confirmed: boolean = false) => {
|
||||
if (!confirmed && hasBreakingChange) {
|
||||
setOpenUpdateModal(true);
|
||||
return;
|
||||
}
|
||||
setLoadingUpdate(true);
|
||||
takeSnapshot();
|
||||
|
||||
const thisNodeTemplate = templates[data.type]?.template;
|
||||
if (!thisNodeTemplate?.code) return;
|
||||
const thisNodeTemplate = templates[data.type]?.template;
|
||||
if (!thisNodeTemplate?.code) return;
|
||||
|
||||
const currentCode = thisNodeTemplate.code.value;
|
||||
if (data.node) {
|
||||
validateComponentCode(
|
||||
{ code: currentCode, frontend_node: data.node },
|
||||
{
|
||||
onSuccess: ({ data: resData, type }) => {
|
||||
if (resData && type && updateNodeCode) {
|
||||
const newNode = processNodeAdvancedFields(
|
||||
resData,
|
||||
edges,
|
||||
data.id,
|
||||
);
|
||||
updateNodeCode(newNode, currentCode, "code", type);
|
||||
const currentCode = thisNodeTemplate.code.value;
|
||||
if (data.node) {
|
||||
validateComponentCode(
|
||||
{ code: currentCode, frontend_node: data.node },
|
||||
{
|
||||
onSuccess: ({ data: resData, type }) => {
|
||||
if (resData && type && updateNodeCode) {
|
||||
const newNode = processNodeAdvancedFields(
|
||||
resData,
|
||||
edges,
|
||||
data.id,
|
||||
);
|
||||
updateNodeCode(newNode, currentCode, "code", type);
|
||||
removeDismissedNodes([data.id]);
|
||||
setLoadingUpdate(false);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setErrorData({
|
||||
title: "Error updating Component code",
|
||||
list: [
|
||||
"There was an error updating the Component.",
|
||||
"If the error persists, please report it on our Discord or GitHub.",
|
||||
],
|
||||
});
|
||||
console.error(error);
|
||||
setLoadingUpdate(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
onError: (error) => {
|
||||
setErrorData({
|
||||
title: "Error updating Component code",
|
||||
list: [
|
||||
"There was an error updating the Component.",
|
||||
"If the error persists, please report it on our Discord or GitHub.",
|
||||
],
|
||||
});
|
||||
console.error(error);
|
||||
setLoadingUpdate(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
templates,
|
||||
edges,
|
||||
updateNodeCode,
|
||||
validateComponentCode,
|
||||
setErrorData,
|
||||
takeSnapshot,
|
||||
]);
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
data,
|
||||
templates,
|
||||
hasBreakingChange,
|
||||
edges,
|
||||
updateNodeCode,
|
||||
validateComponentCode,
|
||||
setErrorData,
|
||||
takeSnapshot,
|
||||
],
|
||||
);
|
||||
|
||||
const handleUpdateCodeWShortcut = useCallback(() => {
|
||||
if (isOutdated && selected) {
|
||||
|
|
@ -194,11 +227,6 @@ function GenericNode({
|
|||
[data.node?.outputs, data.node?.tool_mode],
|
||||
);
|
||||
|
||||
const hasToolMode = useMemo(
|
||||
() => checkHasToolMode(data.node?.template ?? {}),
|
||||
[data.node?.template],
|
||||
);
|
||||
|
||||
const hasOutputs = useMemo(
|
||||
() => data.node?.outputs && data.node.outputs.length > 0,
|
||||
[data.node?.outputs],
|
||||
|
|
@ -270,6 +298,11 @@ function GenericNode({
|
|||
return useFlowStore.getState().nodes.filter((node) => node.selected).length;
|
||||
}, [selected]);
|
||||
|
||||
const shouldShowUpdateComponent = useMemo(
|
||||
() => (isOutdated || hasBreakingChange) && !isUserEdited && !dismissAll,
|
||||
[isOutdated, hasBreakingChange, isUserEdited, dismissAll],
|
||||
);
|
||||
|
||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||
return selected && selectedNodesCount === 1 ? (
|
||||
<>
|
||||
|
|
@ -295,8 +328,10 @@ function GenericNode({
|
|||
showNode={showNode}
|
||||
openAdvancedModal={false}
|
||||
onCloseAdvancedModal={() => {}}
|
||||
updateNode={handleUpdateCode}
|
||||
isOutdated={isOutdated && isUserEdited}
|
||||
updateNode={() => handleUpdateCode()}
|
||||
isOutdated={isOutdated && dismissAll}
|
||||
isUserEdited={isUserEdited}
|
||||
hasBreakingChange={hasBreakingChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="-z-10">
|
||||
|
|
@ -410,10 +445,11 @@ function GenericNode({
|
|||
selected={selected}
|
||||
setBorderColor={setBorderColor}
|
||||
buildStatus={buildStatus}
|
||||
dismissAll={dismissAll}
|
||||
isOutdated={isOutdated}
|
||||
isUserEdited={isUserEdited}
|
||||
isBreakingChange={hasBreakingChange}
|
||||
getValidationStatus={getValidationStatus}
|
||||
handleUpdateComponent={handleUpdateCode}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
|
|
@ -463,42 +499,30 @@ function GenericNode({
|
|||
}, [data, types, isToolMode, showNode, shownOutputs, showHiddenOutputs]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isOutdated && !isUserEdited && !dismissAll ? "relative -mt-10" : "",
|
||||
)}
|
||||
>
|
||||
<div className={cn(shouldShowUpdateComponent ? "relative -mt-10" : "")}>
|
||||
<div
|
||||
className={cn(
|
||||
borderColor,
|
||||
showNode ? "w-80" : `w-48`,
|
||||
"generic-node-div group/node relative rounded-xl shadow-sm hover:shadow-md",
|
||||
"generic-node-div group/node relative rounded-xl border shadow-sm hover:shadow-md",
|
||||
!hasOutputs && "pb-4",
|
||||
)}
|
||||
>
|
||||
<UpdateComponentModal
|
||||
open={openUpdateModal}
|
||||
setOpen={setOpenUpdateModal}
|
||||
onUpdateNode={() => handleUpdateCode(true)}
|
||||
components={componentUpdate ? [componentUpdate] : []}
|
||||
/>
|
||||
{memoizedNodeToolbarComponent}
|
||||
{isOutdated && !isUserEdited && !dismissAll && (
|
||||
<div className="flex h-10 w-full items-center gap-4 rounded-t-[0.69rem] bg-warning p-2 px-4 text-warning-foreground">
|
||||
<ForwardedIconComponent
|
||||
name="AlertTriangle"
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
className="icon-size shrink-0"
|
||||
/>
|
||||
<span className="flex-1 truncate text-sm font-medium">
|
||||
{showNode && "Update Ready"}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="warning"
|
||||
size="iconMd"
|
||||
className="shrink-0 px-2.5 text-xs"
|
||||
onClick={handleUpdateCode}
|
||||
loading={loadingUpdate}
|
||||
data-testid="update-button"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
{shouldShowUpdateComponent && (
|
||||
<NodeUpdateComponent
|
||||
hasBreakingChange={hasBreakingChange}
|
||||
showNode={showNode}
|
||||
handleUpdateCode={() => handleUpdateCode()}
|
||||
loadingUpdate={loadingUpdate}
|
||||
setDismissAll={() => addDismissedNodes([data.id])}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
data-testid={`${data.id}-main-node`}
|
||||
|
|
@ -529,8 +553,7 @@ function GenericNode({
|
|||
{!showNode && (
|
||||
<>
|
||||
{renderInputParameters()}
|
||||
{shownOutputs &&
|
||||
shownOutputs.length > 0 &&
|
||||
{shownOutputs.length > 0 &&
|
||||
renderOutputs(shownOutputs, "render-outputs")}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
169
src/frontend/src/CustomNodes/helpers/check-code-validity.ts
Normal file
169
src/frontend/src/CustomNodes/helpers/check-code-validity.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { componentsToIgnoreUpdate } from "@/constants/constants";
|
||||
import { OutputFieldType } from "@/types/api";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
|
||||
// Returns true if the code is outdated (code string changed and not ignored)
|
||||
const codeIsOutdated = (
|
||||
currentCode: string,
|
||||
thisNodesCode: string,
|
||||
type: string,
|
||||
): boolean => {
|
||||
return !!(
|
||||
currentCode &&
|
||||
thisNodesCode &&
|
||||
currentCode !== thisNodesCode &&
|
||||
!componentsToIgnoreUpdate.includes(type)
|
||||
);
|
||||
};
|
||||
|
||||
// Returns true if there is a breaking change (outputs, template keys, or input_types)
|
||||
const codeHasBreakingChange = (
|
||||
originalOutputs?: OutputFieldType[],
|
||||
userOutputs?: OutputFieldType[],
|
||||
originalTemplate?: { [key: string]: any },
|
||||
userTemplate?: { [key: string]: any },
|
||||
): boolean => {
|
||||
// Check outputs
|
||||
if (
|
||||
originalOutputs &&
|
||||
userOutputs &&
|
||||
!outputsAreEqual(originalOutputs, userOutputs)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Check template keys
|
||||
if (
|
||||
originalTemplate &&
|
||||
userTemplate &&
|
||||
!templateKeysEqual(originalTemplate, userTemplate)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Check input_types containment
|
||||
if (
|
||||
originalTemplate &&
|
||||
userTemplate &&
|
||||
!inputTypesContained(originalTemplate, userTemplate)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const checkCodeValidity = (
|
||||
data: NodeDataType,
|
||||
templates: { [key: string]: any },
|
||||
) => {
|
||||
if (!data?.node || !templates) return;
|
||||
const template = templates[data.type]?.template;
|
||||
const currentCode = template?.code?.value;
|
||||
const thisNodesCode = data.node!.template?.code?.value;
|
||||
const originalOutputs = templates[data.type]?.outputs;
|
||||
const userOutputs = data.node?.outputs;
|
||||
const originalTemplate = template;
|
||||
const userTemplate = data.node?.template;
|
||||
const isOutdated = codeIsOutdated(currentCode, thisNodesCode, data.type);
|
||||
|
||||
const hasBreakingChange = isOutdated
|
||||
? codeHasBreakingChange(
|
||||
originalOutputs,
|
||||
userOutputs,
|
||||
originalTemplate,
|
||||
userTemplate,
|
||||
)
|
||||
: false;
|
||||
|
||||
return {
|
||||
outdated: isOutdated,
|
||||
breakingChange: hasBreakingChange,
|
||||
userEdited: data.node?.edited ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
// templates[data.type]?.template is the original component while data.node.template is the user's component
|
||||
|
||||
// The codeIsOutdated function will have many checks to make sure the code is outdated
|
||||
// the first check is if the current code is defined
|
||||
// the second check is if the data.node.outputs are equal to templates[data.type]?.outputs
|
||||
// and the data.node.template keys are equal to templates[data.type]?.template keys
|
||||
// and all original input_types in each field are contained in the data.node.template input_types. If so, it means it won't break the component
|
||||
// this is a breaking change so we will need to handle it
|
||||
|
||||
// Deep comparison for outputs (order-independent, returns object with per-output match status)
|
||||
const outputsComparisonResult = (
|
||||
originalOutputs: OutputFieldType[] = [],
|
||||
userOutputs: OutputFieldType[] = [],
|
||||
): { [outputName: string]: boolean } => {
|
||||
// Create a map for quick lookup by 'name'
|
||||
const userOutputMap = new Map<string, OutputFieldType>();
|
||||
userOutputs.forEach((output) => {
|
||||
userOutputMap.set(output.name, output);
|
||||
});
|
||||
|
||||
// Build an object with per-output match status
|
||||
const result: { [outputName: string]: boolean } = {};
|
||||
|
||||
originalOutputs.forEach((orig) => {
|
||||
const user = userOutputMap.get(orig.name);
|
||||
result[orig.name] =
|
||||
!!user &&
|
||||
orig.display_name === user.display_name &&
|
||||
JSON.stringify(orig.types) === JSON.stringify(user.types) &&
|
||||
orig.method === user.method &&
|
||||
orig.allows_loop === user.allows_loop;
|
||||
});
|
||||
|
||||
// Check if all user outputs are present in original outputs
|
||||
userOutputs.forEach((user) => {
|
||||
if (!result[user.name]) {
|
||||
result[user.name] = false;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const outputsAreEqual = (
|
||||
originalOutputs: OutputFieldType[],
|
||||
userOutputs: OutputFieldType[],
|
||||
): boolean => {
|
||||
const result = outputsComparisonResult(originalOutputs, userOutputs);
|
||||
// Object.values is more direct for checking all values
|
||||
return Object.values(result).every(Boolean);
|
||||
};
|
||||
|
||||
// Helper to check if all input_types in original are contained in user
|
||||
const inputTypesContained = (
|
||||
originalTemplate: { [key: string]: any },
|
||||
userTemplate: { [key: string]: any },
|
||||
): boolean => {
|
||||
for (const key of Object.keys(originalTemplate)) {
|
||||
const origField = originalTemplate[key];
|
||||
const userField = userTemplate[key];
|
||||
if (!userField) return false;
|
||||
if (origField.input_types) {
|
||||
const origTypes = Array.isArray(origField.input_types)
|
||||
? origField.input_types
|
||||
: [];
|
||||
const userTypes = Array.isArray(userField.input_types)
|
||||
? userField.input_types
|
||||
: [];
|
||||
if (!origTypes.every((t) => userTypes.includes(t))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Helper to check if template keys are equal
|
||||
const templateKeysEqual = (
|
||||
originalTemplate: { [key: string]: any },
|
||||
userTemplate: { [key: string]: any },
|
||||
): boolean => {
|
||||
const origKeys = Object.keys(originalTemplate).sort();
|
||||
const userKeys = Object.keys(userTemplate).sort();
|
||||
return JSON.stringify(origKeys) === JSON.stringify(userKeys);
|
||||
};
|
||||
|
||||
export default checkCodeValidity;
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { componentsToIgnoreUpdate } from "@/constants/constants";
|
||||
import { useEffect } from "react";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
|
||||
const useCheckCodeValidity = (
|
||||
data: NodeDataType,
|
||||
templates: { [key: string]: any },
|
||||
setIsOutdated: (value: boolean) => void,
|
||||
setIsUserEdited: (value: boolean) => void,
|
||||
types,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
// This one should run only once
|
||||
// first check if data.type in NATIVE_CATEGORIES
|
||||
// if not return
|
||||
if (!data?.node || !templates) return;
|
||||
const currentCode = templates[data.type]?.template?.code?.value;
|
||||
const thisNodesCode = data.node!.template?.code?.value;
|
||||
setIsOutdated(
|
||||
currentCode &&
|
||||
thisNodesCode &&
|
||||
currentCode !== thisNodesCode &&
|
||||
!componentsToIgnoreUpdate.includes(data.type),
|
||||
);
|
||||
setIsUserEdited(data.node?.edited ?? false);
|
||||
// template.code can be undefined
|
||||
}, [
|
||||
data.node,
|
||||
data.node?.template?.code?.value,
|
||||
templates,
|
||||
setIsOutdated,
|
||||
setIsUserEdited,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useCheckCodeValidity;
|
||||
|
|
@ -8,8 +8,6 @@ const useUpdateNodeCode = (
|
|||
dataId: string,
|
||||
dataNode: APIClassType, // Define YourNodeType according to your data structure
|
||||
setNode: (id: string, callback: (oldNode) => any) => void,
|
||||
setIsOutdated: (value: boolean) => void,
|
||||
setIsUserEdited: (value: boolean) => void,
|
||||
updateNodeInternals: (id: string) => void,
|
||||
) => {
|
||||
const { setComponentsToUpdate } = useFlowStore();
|
||||
|
|
@ -30,8 +28,6 @@ const useUpdateNodeCode = (
|
|||
}
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
setIsOutdated(false);
|
||||
setIsUserEdited(false);
|
||||
|
||||
const outputs = dataNode.outputs;
|
||||
const updatedOutputs = newNodeClass.outputs;
|
||||
|
|
@ -44,10 +40,12 @@ const useUpdateNodeCode = (
|
|||
return newNode;
|
||||
});
|
||||
|
||||
setComponentsToUpdate((old) => old.filter((id) => id !== dataId));
|
||||
setComponentsToUpdate((old) =>
|
||||
old.filter((component) => component.id !== dataId),
|
||||
);
|
||||
updateNodeInternals(dataId);
|
||||
},
|
||||
[dataId, dataNode, setNode, setIsOutdated, updateNodeInternals],
|
||||
[dataId, dataNode, setNode, updateNodeInternals],
|
||||
);
|
||||
|
||||
return updateNodeCode;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { CustomProductSelector } from "@/customization/components/custom-product
|
|||
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
|
||||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import useTheme from "@/customization/hooks/use-custom-theme";
|
||||
import { useResetDismissUpdateAll } from "@/hooks/use-reset-dismiss-update-all";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AccountMenu } from "./components/AccountMenu";
|
||||
|
|
@ -43,8 +42,6 @@ export default function AppHeader(): JSX.Element {
|
|||
};
|
||||
}, []);
|
||||
|
||||
useResetDismissUpdateAll();
|
||||
|
||||
const getNotificationBadge = () => {
|
||||
const baseClasses = "absolute h-1 w-1 rounded-full bg-destructive";
|
||||
return notificationCenter
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ const useAddFlow = () => {
|
|||
const setFlows = useFlowsManagerStore((state) => state.setFlows);
|
||||
const { deleteFlow } = useDeleteFlow();
|
||||
|
||||
const { setFlowToCanvas } = useFlowsManagerStore();
|
||||
const setNoticeData = useAlertStore.getState().setNoticeData;
|
||||
const { folderId } = useParams();
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
|
|
@ -93,7 +92,6 @@ const useAddFlow = () => {
|
|||
}),
|
||||
}));
|
||||
|
||||
setFlowToCanvas(createdFlow);
|
||||
resolve(createdFlow.id);
|
||||
},
|
||||
onError: (error) => {
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useUtilityStore } from "../stores/utilityStore";
|
||||
export const useResetDismissUpdateAll = () => {
|
||||
const location = useLocation();
|
||||
const flowLocationPath = location.pathname.includes("flow");
|
||||
const setDismissAll = useUtilityStore((state) => state.setDismissAll);
|
||||
|
||||
useEffect(() => {
|
||||
if (flowLocationPath) {
|
||||
setDismissAll(false);
|
||||
}
|
||||
}, [location]);
|
||||
};
|
||||
|
|
@ -18,6 +18,10 @@ export const switchCaseModalSize = (size: string) => {
|
|||
minWidth = "min-w-[40vw]";
|
||||
height = "";
|
||||
break;
|
||||
case "small-update":
|
||||
minWidth = "min-w-[480px] max-w-[480px]";
|
||||
height = "";
|
||||
break;
|
||||
case "small":
|
||||
minWidth = "min-w-[40vw]";
|
||||
height = "h-[40vh]";
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ interface BaseModalProps {
|
|||
| "retangular"
|
||||
| "smaller"
|
||||
| "small"
|
||||
| "small-update"
|
||||
| "small-query"
|
||||
| "medium"
|
||||
| "medium-tall"
|
||||
|
|
|
|||
225
src/frontend/src/modals/updateComponentModal/index.tsx
Normal file
225
src/frontend/src/modals/updateComponentModal/index.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import TableComponent from "@/components/core/parameterRenderComponent/components/tableComponent";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import useDuplicateFlows from "@/pages/MainPage/hooks/use-handle-duplicate";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { ComponentsToUpdateType } from "@/types/zustand/flow";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { ColDef } from "ag-grid-community";
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function UpdateComponentModal({
|
||||
open,
|
||||
setOpen,
|
||||
onUpdateNode,
|
||||
children,
|
||||
components,
|
||||
isMultiple = false,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onUpdateNode: (updatedComponents?: string[]) => void;
|
||||
children?: React.ReactNode;
|
||||
components: ComponentsToUpdateType[];
|
||||
isMultiple?: boolean;
|
||||
}) {
|
||||
const [backupFlow, setBackupFlow] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [selectedComponents, setSelectedComponents] = useState<Set<string>>(
|
||||
new Set(components.filter((c) => !c.breakingChange).map((c) => c.id)),
|
||||
);
|
||||
const agGrid = useRef<AgGridReact>(null);
|
||||
const currentFlow = useFlowStore((state) => state.currentFlow);
|
||||
|
||||
const { handleDuplicate } = useDuplicateFlows({
|
||||
flow: currentFlow
|
||||
? { ...currentFlow, name: currentFlow.name + " (Backup)" }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const handleUpdate = () => {
|
||||
setLoading(true);
|
||||
if (backupFlow) {
|
||||
handleDuplicate().then(() => {
|
||||
onUpdateNode(
|
||||
components.length > 0 ? Array.from(selectedComponents) : undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
setOpen(false);
|
||||
});
|
||||
} else {
|
||||
onUpdateNode(
|
||||
components.length > 0 ? Array.from(selectedComponents) : undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columnDefs: ColDef[] = [
|
||||
{ field: "id", hide: true },
|
||||
{
|
||||
headerName: "Component",
|
||||
field: "display_name",
|
||||
headerClass: "!text-mmd !font-normal",
|
||||
flex: 1,
|
||||
headerCheckboxSelection: true,
|
||||
checkboxSelection: true,
|
||||
resizable: false,
|
||||
cellRenderer: (params) => {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{params.data.icon && (
|
||||
<ForwardedIconComponent
|
||||
name={params.data.icon}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
{params.value}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: "Update Type",
|
||||
field: "breakingChange",
|
||||
headerClass: "!text-mmd !font-normal",
|
||||
resizable: false,
|
||||
flex: 1,
|
||||
cellClass: "text-muted-foreground",
|
||||
cellRenderer: (params) => {
|
||||
return params.value ? (
|
||||
<span className="font-semibold text-accent-amber-foreground">
|
||||
Breaking
|
||||
</span>
|
||||
) : (
|
||||
<span>Standard</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setBackupFlow(true);
|
||||
setSelectedComponents(
|
||||
new Set(components.filter((c) => !c.breakingChange).map((c) => c.id)),
|
||||
);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (agGrid.current) {
|
||||
agGrid.current?.api?.forEachNode((node) => {
|
||||
if (selectedComponents.has(node.data.id)) {
|
||||
node.setSelected(true);
|
||||
} else {
|
||||
node.setSelected(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [agGrid.current, selectedComponents, open]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
closeButtonClassName="!top-2 !right-3"
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
size="small-update"
|
||||
className="px-4 py-3"
|
||||
>
|
||||
<BaseModal.Trigger asChild>{children ?? <></>}</BaseModal.Trigger>
|
||||
<BaseModal.Header>
|
||||
<span className="">
|
||||
Update{" "}
|
||||
{isMultiple ? "components" : (components?.[0]?.display_name ?? "")}
|
||||
</span>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content overflowHidden>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3 text-sm text-muted-foreground">
|
||||
{isMultiple ? (
|
||||
<p>
|
||||
Updates marked as{" "}
|
||||
<span className="font-semibold text-accent-amber-foreground">
|
||||
breaking
|
||||
</span>{" "}
|
||||
may change inputs, outputs, or component behavior. In some
|
||||
cases, they will disconnect components from your flow, requiring
|
||||
you to review or reconnect them afterward. Components added from
|
||||
the sidebar always use the latest version.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
This update may change inputs, outputs, or component behavior.
|
||||
In some cases, it will{" "}
|
||||
<span className="font-semibold text-accent-amber-foreground">
|
||||
disconnect this component from your flow
|
||||
</span>
|
||||
, requiring you to review or reconnect it afterward.
|
||||
</p>
|
||||
<p>
|
||||
Components added from the sidebar always use the latest
|
||||
version.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isMultiple && (
|
||||
<div className="-mx-4">
|
||||
<TableComponent
|
||||
columnDefs={columnDefs}
|
||||
ref={agGrid}
|
||||
domLayout="autoHeight"
|
||||
rowData={components}
|
||||
rowSelection="multiple"
|
||||
className="ag-tool-mode ag-no-selection"
|
||||
rowHeight={30}
|
||||
headerHeight={30}
|
||||
suppressRowClickSelection={false}
|
||||
onSelectionChanged={(event) => {
|
||||
const selectedIds = event.api
|
||||
.getSelectedRows()
|
||||
.map((row) => row.id);
|
||||
setSelectedComponents(new Set(selectedIds));
|
||||
}}
|
||||
suppressRowHoverHighlight={true}
|
||||
tableOptions={{ hide_options: true }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"mb-3 flex items-center gap-3 rounded-md border p-3 text-sm transition-all",
|
||||
!backupFlow && "border-accent-amber-foreground bg-accent-amber",
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={backupFlow}
|
||||
onCheckedChange={(checked) =>
|
||||
setBackupFlow(checked === "indeterminate" ? false : checked)
|
||||
}
|
||||
className="bg-muted"
|
||||
id="backupFlow"
|
||||
data-testid="backup-flow-checkbox"
|
||||
/>
|
||||
<label htmlFor="backupFlow" className="cursor-pointer select-none">
|
||||
Create backup flow before updating
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer
|
||||
submit={{
|
||||
label: "Update Component" + (components.length > 1 ? "s" : ""),
|
||||
onClick: handleUpdate,
|
||||
disabled: isMultiple && selectedComponents.size === 0,
|
||||
loading,
|
||||
}}
|
||||
></BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +1,17 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
||||
import { processNodeAdvancedFields } from "@/CustomNodes/helpers/process-node-advanced-fields";
|
||||
import useUpdateAllNodes, {
|
||||
UpdateNodesType,
|
||||
} from "@/CustomNodes/hooks/use-update-all-nodes";
|
||||
import UpdateComponentModal from "@/modals/updateComponentModal";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { useTypesStore } from "@/stores/typesStore";
|
||||
import { useUtilityStore } from "@/stores/utilityStore";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { useUpdateNodeInternals } from "@xyflow/react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
|
||||
const ERROR_MESSAGE_UPDATING_COMPONENTS = "Error updating components";
|
||||
const ERROR_MESSAGE_UPDATING_COMPONENTS_LIST = [
|
||||
|
|
@ -24,7 +23,6 @@ const ERROR_MESSAGE_EDGES_LOST =
|
|||
|
||||
export default function UpdateAllComponents({}: {}) {
|
||||
const { componentsToUpdate, nodes, edges, setNodes } = useFlowStore();
|
||||
const setDismissAll = useUtilityStore((state) => state.setDismissAll);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const { mutateAsync: validateComponentCode } = usePostValidateComponentCode();
|
||||
|
|
@ -34,7 +32,26 @@ export default function UpdateAllComponents({}: {}) {
|
|||
const updateAllNodes = useUpdateAllNodes(setNodes, updateNodeInternals);
|
||||
|
||||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dismissedNodes = useFlowStore((state) => state.dismissedNodes);
|
||||
const addDismissedNodes = useFlowStore((state) => state.addDismissedNodes);
|
||||
|
||||
const dismissed = useMemo(
|
||||
() =>
|
||||
componentsToUpdate.every((component) =>
|
||||
dismissedNodes.includes(component.id),
|
||||
),
|
||||
[dismissedNodes, componentsToUpdate],
|
||||
);
|
||||
|
||||
const componentsToUpdateFiltered = useMemo(
|
||||
() =>
|
||||
componentsToUpdate.filter(
|
||||
(component) => !dismissedNodes.includes(component.id),
|
||||
),
|
||||
[componentsToUpdate, dismissedNodes],
|
||||
);
|
||||
|
||||
const edgesUpdateRef = useRef({
|
||||
numberOfEdgesBeforeUpdate: 0,
|
||||
|
|
@ -62,7 +79,15 @@ export default function UpdateAllComponents({}: {}) {
|
|||
}`;
|
||||
};
|
||||
|
||||
const handleUpdateAllComponents = () => {
|
||||
const breakingChanges = componentsToUpdateFiltered.filter(
|
||||
(component) => component.breakingChange,
|
||||
);
|
||||
|
||||
const handleUpdateAllComponents = (confirmed?: boolean, ids?: string[]) => {
|
||||
if (!confirmed && breakingChanges.length > 0) {
|
||||
setIsOpen(true);
|
||||
return;
|
||||
}
|
||||
startEdgesUpdateRef();
|
||||
|
||||
setLoadingUpdate(true);
|
||||
|
|
@ -71,42 +96,48 @@ export default function UpdateAllComponents({}: {}) {
|
|||
let updatedCount = 0;
|
||||
const updates: UpdateNodesType[] = [];
|
||||
|
||||
const updatePromises = componentsToUpdate.map((nodeId) => {
|
||||
const node = nodes.find((n) => n.id === nodeId);
|
||||
if (!node || node.type !== "genericNode") return Promise.resolve();
|
||||
const updatePromises = componentsToUpdateFiltered
|
||||
.filter((component) => ids?.includes(component.id) ?? true)
|
||||
.map((nodeUpdate) => {
|
||||
const node = nodes.find((n) => n.id === nodeUpdate.id);
|
||||
if (!node || node.type !== "genericNode") return Promise.resolve();
|
||||
|
||||
const thisNodeTemplate = templates[node.data.type]?.template;
|
||||
if (!thisNodeTemplate?.code) return Promise.resolve();
|
||||
const thisNodeTemplate = templates[node.data.type]?.template;
|
||||
if (!thisNodeTemplate?.code) return Promise.resolve();
|
||||
|
||||
const currentCode = thisNodeTemplate.code.value;
|
||||
const currentCode = thisNodeTemplate.code.value;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
validateComponentCode({
|
||||
code: currentCode,
|
||||
frontend_node: node.data.node!,
|
||||
})
|
||||
.then(({ data: resData, type }) => {
|
||||
if (resData && type) {
|
||||
const newNode = processNodeAdvancedFields(resData, edges, nodeId);
|
||||
|
||||
updates.push({
|
||||
nodeId,
|
||||
newNode,
|
||||
code: currentCode,
|
||||
name: "code",
|
||||
type,
|
||||
});
|
||||
|
||||
updatedCount++;
|
||||
}
|
||||
resolve(null);
|
||||
return new Promise((resolve) => {
|
||||
validateComponentCode({
|
||||
code: currentCode,
|
||||
frontend_node: node.data.node!,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
resolve(null);
|
||||
});
|
||||
.then(({ data: resData, type }) => {
|
||||
if (resData && type) {
|
||||
const newNode = processNodeAdvancedFields(
|
||||
resData,
|
||||
edges,
|
||||
nodeUpdate.id,
|
||||
);
|
||||
|
||||
updates.push({
|
||||
nodeId: nodeUpdate.id,
|
||||
newNode,
|
||||
code: currentCode,
|
||||
name: "code",
|
||||
type,
|
||||
});
|
||||
|
||||
updatedCount++;
|
||||
}
|
||||
resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(updatePromises)
|
||||
.then(() => {
|
||||
|
|
@ -144,50 +175,59 @@ export default function UpdateAllComponents({}: {}) {
|
|||
};
|
||||
};
|
||||
|
||||
if (componentsToUpdate.length === 0) return null;
|
||||
if (componentsToUpdateFiltered.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-2 left-1/2 z-50 flex w-[500px] -translate-x-1/2 items-center gap-8 rounded-lg bg-warning px-4 py-2 text-sm font-medium text-warning-foreground shadow-md transition-all ease-in",
|
||||
"absolute bottom-2 left-1/2 z-50 flex w-[530px] -translate-x-1/2 items-center justify-between gap-8 rounded-lg border bg-background px-4 py-2 text-sm font-medium shadow-md transition-all ease-in",
|
||||
dismissed && "translate-y-[120%]",
|
||||
componentsToUpdateFiltered.some(
|
||||
(component) => component.breakingChange,
|
||||
) && "border-accent-amber-foreground",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<ForwardedIconComponent
|
||||
name="AlertTriangle"
|
||||
className="!h-[18px] !w-[18px] shrink-0"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<span>
|
||||
{componentsToUpdate.length} component
|
||||
{componentsToUpdate.length > 1 ? "s are" : " is"} ready to update
|
||||
Update
|
||||
{componentsToUpdateFiltered.length > 1 ? "s are" : " is"} available
|
||||
for{" "}
|
||||
{componentsToUpdateFiltered.length +
|
||||
" component" +
|
||||
(componentsToUpdateFiltered.length > 1 ? "s" : "")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="shrink-0 text-sm text-warning-foreground"
|
||||
className="shrink-0 text-sm"
|
||||
onClick={(e) => {
|
||||
setDismissed(true);
|
||||
setDismissAll(true);
|
||||
addDismissedNodes(
|
||||
componentsToUpdateFiltered.map((component) => component.id),
|
||||
);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
Dismiss {componentsToUpdateFiltered.length > 1 ? "All" : ""}
|
||||
</Button>
|
||||
<Button
|
||||
variant="warning"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={handleUpdateAllComponents}
|
||||
onClick={() => handleUpdateAllComponents()}
|
||||
loading={loadingUpdate}
|
||||
data-testid="update-all-button"
|
||||
>
|
||||
Update All
|
||||
{breakingChanges.length > 0 ? "Review All" : "Update All"}
|
||||
</Button>
|
||||
</div>
|
||||
<UpdateComponentModal
|
||||
isMultiple={true}
|
||||
open={isOpen}
|
||||
setOpen={setIsOpen}
|
||||
onUpdateNode={(ids) => handleUpdateAllComponents(true, ids)}
|
||||
components={componentsToUpdateFiltered}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ const NodeToolbarComponent = memo(
|
|||
onCloseAdvancedModal,
|
||||
updateNode,
|
||||
isOutdated,
|
||||
isUserEdited,
|
||||
hasBreakingChange,
|
||||
setOpenShowMoreOptions,
|
||||
}: nodeToolbarPropsType): JSX.Element => {
|
||||
const version = useDarkStore((state) => state.version);
|
||||
|
|
@ -102,8 +104,6 @@ const NodeToolbarComponent = memo(
|
|||
Object.values(flow).includes(data.node?.display_name!),
|
||||
);
|
||||
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
const nodeLength = useMemo(() => getNodeLength(data), [data]);
|
||||
const hasCode = useMemo(
|
||||
() => Object.keys(data.node!.template).includes("code"),
|
||||
|
|
@ -606,8 +606,9 @@ const NodeToolbarComponent = memo(
|
|||
shortcuts.find((obj) => obj.name === "Update")
|
||||
?.shortcut!
|
||||
}
|
||||
value={"Restore"}
|
||||
icon={"RefreshCcwDot"}
|
||||
style={hasBreakingChange ? "text-warning" : ""}
|
||||
value={isUserEdited ? "Restore" : "Update"}
|
||||
icon={isUserEdited ? "RefreshCcwDot" : "CircleArrowUp"}
|
||||
dataTestId="update-button-modal"
|
||||
/>
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
|
||||
const flowToCanvas = useFlowsManagerStore((state) => state.flowToCanvas);
|
||||
|
||||
const updatedAt = currentSavedFlow?.updated_at;
|
||||
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
|
||||
const stopBuilding = useFlowStore((state) => state.stopBuilding);
|
||||
|
|
@ -112,19 +110,18 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
|
||||
const isAnExistingFlowId = isAnExistingFlow.id;
|
||||
|
||||
flowToCanvas
|
||||
? setCurrentFlow(flowToCanvas)
|
||||
: getFlowToAddToCanvas(isAnExistingFlowId);
|
||||
await getFlowToAddToCanvas(isAnExistingFlowId);
|
||||
}
|
||||
};
|
||||
awaitgetTypes();
|
||||
}, [id, flows, currentFlowId, flowToCanvas]);
|
||||
}, [id, flows, currentFlowId]);
|
||||
|
||||
useEffect(() => {
|
||||
setOnFlowPage(true);
|
||||
|
||||
return () => {
|
||||
setOnFlowPage(false);
|
||||
console.log("unmounting");
|
||||
setCurrentFlow(undefined);
|
||||
};
|
||||
}, [id]);
|
||||
|
|
@ -152,6 +149,7 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
|
||||
const getFlowToAddToCanvas = async (id: string) => {
|
||||
const flow = await getFlow({ id: id });
|
||||
console.log(flow);
|
||||
setCurrentFlow(flow);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|||
import useAlertStore from "@/stores/alertStore";
|
||||
import { FlowType } from "@/types/flow";
|
||||
import { downloadFlow } from "@/utils/reactflowUtils";
|
||||
import useDuplicateFlows from "../../hooks/use-handle-duplicate";
|
||||
import useDuplicateFlow from "../../hooks/use-handle-duplicate";
|
||||
import useSelectOptionsChange from "../../hooks/use-select-options-change";
|
||||
|
||||
type DropdownComponentProps = {
|
||||
|
|
@ -21,11 +21,15 @@ const DropdownComponent = ({
|
|||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const { handleDuplicate } = useDuplicateFlows({
|
||||
selectedFlowsComponentsCards: [flowData.id],
|
||||
allFlows: [flowData],
|
||||
setSuccessData,
|
||||
});
|
||||
const { handleDuplicate } = useDuplicateFlow({ flow: flowData });
|
||||
|
||||
const duplicateFlow = () => {
|
||||
handleDuplicate().then(() =>
|
||||
setSuccessData({
|
||||
title: `${flowData.is_component ? "Component" : "Flow"} duplicated successfully`,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
downloadFlow(flowData, flowData.name, flowData.description);
|
||||
|
|
@ -35,7 +39,7 @@ const DropdownComponent = ({
|
|||
[flowData.id],
|
||||
setErrorData,
|
||||
setOpenDelete,
|
||||
handleDuplicate,
|
||||
duplicateFlow,
|
||||
handleExport,
|
||||
handleEdit,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@ const GridComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const { folderId } = useParams();
|
||||
const isComponent = flowData.is_component ?? false;
|
||||
const setFlowToCanvas = useFlowsManagerStore(
|
||||
(state) => state.setFlowToCanvas,
|
||||
);
|
||||
|
||||
const { getIcon } = useGetTemplateStyle(flowData);
|
||||
|
||||
|
|
@ -50,7 +47,6 @@ const GridComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
|
||||
const handleClick = async () => {
|
||||
if (!isComponent) {
|
||||
await setFlowToCanvas(flowData);
|
||||
navigate(editFlowLink);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,16 +33,13 @@ const ListComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
const { folderId } = useParams();
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const isComponent = flowData.is_component ?? false;
|
||||
const setFlowToCanvas = useFlowsManagerStore(
|
||||
(state) => state.setFlowToCanvas,
|
||||
);
|
||||
|
||||
const { getIcon } = useGetTemplateStyle(flowData);
|
||||
|
||||
const editFlowLink = `/flow/${flowData.id}${folderId ? `/folder/${folderId}` : ""}`;
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!isComponent) {
|
||||
await setFlowToCanvas(flowData);
|
||||
navigate(editFlowLink);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,46 +1,31 @@
|
|||
import { usePostAddFlow } from "@/controllers/API/queries/flows/use-post-add-flow";
|
||||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { addVersionToDuplicates, createNewFlow } from "@/utils/reactflowUtils";
|
||||
import { FlowType } from "@/types/flow";
|
||||
import { createNewFlow } from "@/utils/reactflowUtils";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
type UseDuplicateFlowsParams = {
|
||||
selectedFlowsComponentsCards: string[];
|
||||
allFlows: any[];
|
||||
setSuccessData: (data: { title: string }) => void;
|
||||
flow?: FlowType;
|
||||
};
|
||||
|
||||
const useDuplicateFlows = ({
|
||||
selectedFlowsComponentsCards,
|
||||
allFlows,
|
||||
setSuccessData,
|
||||
}: UseDuplicateFlowsParams) => {
|
||||
const useDuplicateFlow = ({ flow }: UseDuplicateFlowsParams) => {
|
||||
const { mutateAsync: postAddFlow } = usePostAddFlow();
|
||||
const { folderId } = useParams();
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
|
||||
const handleDuplicate = async () => {
|
||||
selectedFlowsComponentsCards.map(async (selectedFlow) => {
|
||||
const currentFlow = allFlows.find((flow) => flow.id === selectedFlow);
|
||||
if (flow?.data) {
|
||||
const folder_id = folderId ?? myCollectionId ?? "";
|
||||
|
||||
const flowsToCheckNames = allFlows?.filter(
|
||||
(f) => f.folder_id === folder_id,
|
||||
);
|
||||
const newFlow = createNewFlow(flow.data, folder_id, flow);
|
||||
|
||||
const newFlow = createNewFlow(currentFlow.data, folder_id, currentFlow);
|
||||
|
||||
const newName = addVersionToDuplicates(newFlow, flowsToCheckNames ?? []);
|
||||
newFlow.name = newName;
|
||||
newFlow.folder_id = folder_id;
|
||||
|
||||
await postAddFlow(newFlow);
|
||||
setSuccessData({
|
||||
title: `${newFlow.is_component ? "Component" : "Flow"} duplicated successfully`,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { handleDuplicate };
|
||||
};
|
||||
|
||||
export default useDuplicateFlows;
|
||||
export default useDuplicateFlow;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import {
|
||||
BROKEN_EDGES_WARNING,
|
||||
componentsToIgnoreUpdate,
|
||||
} from "@/constants/constants";
|
||||
import { BROKEN_EDGES_WARNING } from "@/constants/constants";
|
||||
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
|
||||
import {
|
||||
track,
|
||||
trackDataLoaded,
|
||||
trackFlowBuild,
|
||||
} from "@/customization/utils/analytics";
|
||||
import { checkCodeValidity } from "@/CustomNodes/helpers/check-code-validity";
|
||||
import { brokenEdgeMessage } from "@/utils/utils";
|
||||
import {
|
||||
EdgeChange,
|
||||
|
|
@ -33,7 +31,11 @@ import {
|
|||
sourceHandleType,
|
||||
targetHandleType,
|
||||
} from "../types/flow";
|
||||
import { FlowStoreType, VertexLayerElementType } from "../types/zustand/flow";
|
||||
import {
|
||||
ComponentsToUpdateType,
|
||||
FlowStoreType,
|
||||
VertexLayerElementType,
|
||||
} from "../types/zustand/flow";
|
||||
import { buildFlowVerticesWithFallback } from "../utils/buildUtils";
|
||||
import {
|
||||
buildPositionDictionary,
|
||||
|
|
@ -88,24 +90,22 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
set({ componentsToUpdate: newChange });
|
||||
},
|
||||
updateComponentsToUpdate: (nodes) => {
|
||||
let outdatedNodes: string[] = [];
|
||||
let outdatedNodes: ComponentsToUpdateType[] = [];
|
||||
const templates = useTypesStore.getState().templates;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let node = nodes[i];
|
||||
nodes.forEach((node) => {
|
||||
if (node.type === "genericNode") {
|
||||
const currentCode = templates[node.data?.type]?.template?.code?.value;
|
||||
const thisNodesCode = node.data?.node!.template?.code?.value;
|
||||
if (
|
||||
currentCode &&
|
||||
thisNodesCode &&
|
||||
currentCode !== thisNodesCode &&
|
||||
!node.data?.node?.edited &&
|
||||
!componentsToIgnoreUpdate.includes(node.data?.type)
|
||||
) {
|
||||
outdatedNodes.push(node.id);
|
||||
}
|
||||
const codeValidity = checkCodeValidity(node.data, templates);
|
||||
if (codeValidity && codeValidity.outdated)
|
||||
outdatedNodes.push({
|
||||
id: node.id,
|
||||
icon: node.data.node?.icon,
|
||||
display_name: node.data.node?.display_name,
|
||||
outdated: codeValidity.outdated,
|
||||
breakingChange: codeValidity.breakingChange,
|
||||
userEdited: codeValidity.userEdited,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
set({ componentsToUpdate: outdatedNodes });
|
||||
},
|
||||
onFlowPage: false,
|
||||
|
|
@ -223,6 +223,11 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
let newEdges = cleanEdges(nodes, edges);
|
||||
const { inputs, outputs } = getInputsAndOutputs(nodes);
|
||||
get().updateComponentsToUpdate(nodes);
|
||||
set({
|
||||
dismissedNodes: JSON.parse(
|
||||
localStorage.getItem(`dismiss_${flow?.id}`) ?? "[]",
|
||||
) as string[],
|
||||
});
|
||||
unselectAllNodesEdges(nodes, edges);
|
||||
set({
|
||||
nodes,
|
||||
|
|
@ -992,6 +997,27 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
componentsToUpdate: [],
|
||||
});
|
||||
},
|
||||
dismissedNodes: [],
|
||||
addDismissedNodes: (dismissedNodes: string[]) => {
|
||||
const newDismissedNodes = Array.from(
|
||||
new Set([...get().dismissedNodes, ...dismissedNodes]),
|
||||
);
|
||||
localStorage.setItem(
|
||||
`dismiss_${get().currentFlow?.id}`,
|
||||
JSON.stringify(newDismissedNodes),
|
||||
);
|
||||
set({ dismissedNodes: newDismissedNodes });
|
||||
},
|
||||
removeDismissedNodes: (dismissedNodes: string[]) => {
|
||||
const newDismissedNodes = get().dismissedNodes.filter(
|
||||
(node) => !dismissedNodes.includes(node),
|
||||
);
|
||||
localStorage.setItem(
|
||||
`dismiss_${get().currentFlow?.id}`,
|
||||
JSON.stringify(newDismissedNodes),
|
||||
);
|
||||
set({ dismissedNodes: newDismissedNodes });
|
||||
},
|
||||
}));
|
||||
|
||||
export default useFlowStore;
|
||||
|
|
|
|||
|
|
@ -130,19 +130,11 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
setSelectedFlowsComponentsCards: (selectedFlowsComponentsCards: string[]) => {
|
||||
set({ selectedFlowsComponentsCards });
|
||||
},
|
||||
flowToCanvas: null,
|
||||
setFlowToCanvas: async (flowToCanvas: FlowType | null) => {
|
||||
await new Promise<void>((resolve) => {
|
||||
set({ flowToCanvas });
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
resetStore: () => {
|
||||
set({
|
||||
flows: [],
|
||||
currentFlow: undefined,
|
||||
currentFlowId: "",
|
||||
flowToCanvas: null,
|
||||
searchFlowsComponents: "",
|
||||
selectedFlowsComponentsCards: [],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import { create } from "zustand";
|
|||
export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
|
||||
clientId: "",
|
||||
setClientId: (clientId: string) => set({ clientId }),
|
||||
dismissAll: false,
|
||||
setDismissAll: (dismissAll: boolean) => set({ dismissAll }),
|
||||
chatValueStore: "",
|
||||
setChatValueStore: (value: string) => set({ chatValueStore: value }),
|
||||
selectedItems: [],
|
||||
|
|
|
|||
|
|
@ -135,10 +135,19 @@
|
|||
padding: 0rem 1rem !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-row-focus {
|
||||
.ag-tool-mode:not(.ag-no-selection) .ag-row-focus {
|
||||
background-color: hsl(var(--accent)) !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-row-selected:not(.ag-row-focus)::before {
|
||||
.ag-tool-mode.ag-no-selection .ag-row-selected::before {
|
||||
background-color: hsl(var(--background)) !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-checkbox-input-wrapper:focus-within,
|
||||
.ag-tool-mode .ag-checkbox-input-wrapper:active {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-layout-auto-height .ag-center-cols-container,
|
||||
.ag-tool-mode .ag-layout-auto-height .ag-center-cols-viewport {
|
||||
min-height: 0px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ export type OutputFieldType = {
|
|||
types: Array<string>;
|
||||
selected?: string;
|
||||
name: string;
|
||||
method?: string;
|
||||
display_name: string;
|
||||
hidden?: boolean;
|
||||
proxy?: OutputFieldProxyType;
|
||||
|
|
|
|||
|
|
@ -583,6 +583,8 @@ export type nodeToolbarPropsType = {
|
|||
openAdvancedModal?: boolean;
|
||||
onCloseAdvancedModal?: (close: boolean) => void;
|
||||
isOutdated: boolean;
|
||||
isUserEdited: boolean;
|
||||
hasBreakingChange: boolean;
|
||||
updateNode: () => void;
|
||||
closeToolbar?: () => void;
|
||||
setOpenShowMoreOptions?: (open: boolean) => void;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,19 @@ export type FlowPoolType = {
|
|||
[key: string]: Array<VertexBuildTypeAPI>;
|
||||
};
|
||||
|
||||
export type ComponentsToUpdateType = {
|
||||
id: string;
|
||||
icon?: string;
|
||||
display_name: string;
|
||||
outdated: boolean;
|
||||
breakingChange: boolean;
|
||||
userEdited: boolean;
|
||||
};
|
||||
|
||||
export type FlowStoreType = {
|
||||
dismissedNodes: string[];
|
||||
addDismissedNodes: (dismissedNodes: string[]) => void;
|
||||
removeDismissedNodes: (dismissedNodes: string[]) => void;
|
||||
//key x, y
|
||||
positionDictionary: { [key: number]: number };
|
||||
isPositionAvailable: (position: { x: number; y: number }) => boolean;
|
||||
|
|
@ -61,9 +73,11 @@ export type FlowStoreType = {
|
|||
}) => void;
|
||||
fitViewNode: (nodeId: string) => void;
|
||||
autoSaveFlow: (() => void) | undefined;
|
||||
componentsToUpdate: string[];
|
||||
componentsToUpdate: ComponentsToUpdateType[];
|
||||
setComponentsToUpdate: (
|
||||
update: string[] | ((oldState: string[]) => string[]),
|
||||
update:
|
||||
| ComponentsToUpdateType[]
|
||||
| ((oldState: ComponentsToUpdateType[]) => ComponentsToUpdateType[]),
|
||||
) => void;
|
||||
updateComponentsToUpdate: (nodes: AllNodeType[]) => void;
|
||||
onFlowPage: boolean;
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ export type FlowsManagerStoreType = {
|
|||
setAutoSavingInterval: (autoSavingInterval: number) => void;
|
||||
healthCheckMaxRetries: number;
|
||||
setHealthCheckMaxRetries: (healthCheckMaxRetries: number) => void;
|
||||
flowToCanvas: FlowType | null;
|
||||
setFlowToCanvas: (flowToCanvas: FlowType | null) => Promise<void>;
|
||||
IOModalOpen: boolean;
|
||||
setIOModalOpen: (IOModalOpen: boolean) => void;
|
||||
resetStore: () => void;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ export type UtilityStoreType = {
|
|||
playgroundScrollBehaves: ScrollBehavior;
|
||||
setPlaygroundScrollBehaves: (behaves: ScrollBehavior) => void;
|
||||
maxFileSizeUpload: number;
|
||||
setMaxFileSizeUpload: (maxFileSizeUpload: number) => void;
|
||||
flowsPagination: Pagination;
|
||||
setFlowsPagination: (pagination: Pagination) => void;
|
||||
tags: Tag[];
|
||||
|
|
@ -20,8 +19,6 @@ export type UtilityStoreType = {
|
|||
setWebhookPollingInterval: (webhookPollingInterval: number) => void;
|
||||
chatValueStore: string;
|
||||
setChatValueStore: (value: string) => void;
|
||||
dismissAll: boolean;
|
||||
setDismissAll: (dismissAll: boolean) => void;
|
||||
currentSessionId: string;
|
||||
setCurrentSessionId: (sessionId: string) => void;
|
||||
setClientId: (clientId: string) => void;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,7 +2,9 @@ import { expect, test } from "@playwright/test";
|
|||
import { readFileSync } from "fs";
|
||||
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||
|
||||
test("user must be able to update outdated components", async ({ page }) => {
|
||||
test("user must be able to update outdated components by update all button", async ({
|
||||
page,
|
||||
}) => {
|
||||
await awaitBootstrapTest(page);
|
||||
|
||||
await page.locator("span").filter({ hasText: "Close" }).first().click();
|
||||
|
|
@ -27,22 +29,152 @@ test("user must be able to update outdated components", async ({ page }) => {
|
|||
dataTransfer,
|
||||
});
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForSelector("data-testid=list-card", {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
await page.getByTestId("list-card").first().click();
|
||||
|
||||
await expect(page.getByText("components are ready to update")).toBeVisible({
|
||||
await expect(page.getByText("Updates are available for 5")).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
let outdatedComponents = await page.getByTestId("icon-AlertTriangle").count();
|
||||
expect(outdatedComponents).toBeGreaterThan(0);
|
||||
let outdatedComponents = await page.getByTestId("update-button").count();
|
||||
expect(outdatedComponents).toBe(1);
|
||||
|
||||
await page.getByText("Update All", { exact: true }).click();
|
||||
let outdatedBreakingComponents = await page
|
||||
.getByTestId("review-button")
|
||||
.count();
|
||||
expect(outdatedBreakingComponents).toBe(4);
|
||||
|
||||
await expect(page.getByTestId("icon-AlertTriangle")).toHaveCount(0, {
|
||||
expect(await page.getByTestId("update-all-button")).toHaveText("Review All");
|
||||
|
||||
await page.getByTestId("update-all-button").click();
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(2).isChecked(),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(5).isChecked(),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(6).isChecked(),
|
||||
).toBe(false);
|
||||
|
||||
await page
|
||||
.getByRole("checkbox", { name: "Column with Header Selection" })
|
||||
.check();
|
||||
|
||||
expect(await page.getByTestId("backup-flow-checkbox").isChecked()).toBe(true);
|
||||
await page.getByTestId("backup-flow-checkbox").click();
|
||||
|
||||
await page.getByRole("button", { name: "Update Components" }).click();
|
||||
|
||||
await expect(page.getByTestId("update-button")).toHaveCount(0, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId("review-button")).toHaveCount(0, {
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
test("user must be able to update outdated components by each outdated component", async ({
|
||||
page,
|
||||
}) => {
|
||||
await awaitBootstrapTest(page);
|
||||
|
||||
await page.locator("span").filter({ hasText: "Close" }).first().click();
|
||||
|
||||
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
|
||||
// Read your file into a buffer.
|
||||
const jsonContent = readFileSync("tests/assets/outdated_flow.json", "utf-8");
|
||||
|
||||
// Create the DataTransfer and File
|
||||
const dataTransfer = await page.evaluateHandle((data) => {
|
||||
const dt = new DataTransfer();
|
||||
// Convert the buffer to a hex array
|
||||
const file = new File([data], "outdated_flow.json", {
|
||||
type: "application/json",
|
||||
});
|
||||
dt.items.add(file);
|
||||
return dt;
|
||||
}, jsonContent);
|
||||
|
||||
// Now dispatch
|
||||
await page.getByTestId("cards-wrapper").dispatchEvent("drop", {
|
||||
dataTransfer,
|
||||
});
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForSelector("data-testid=list-card", {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
await page.getByTestId("list-card").first().click();
|
||||
|
||||
await expect(page.getByText("Updates are available for 5")).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
let outdatedComponents = await page.getByTestId("update-button").count();
|
||||
expect(outdatedComponents).toBe(1);
|
||||
|
||||
let outdatedBreakingComponents = await page
|
||||
.getByTestId("review-button")
|
||||
.count();
|
||||
expect(outdatedBreakingComponents).toBe(4);
|
||||
|
||||
expect(await page.getByTestId("update-all-button")).toHaveText("Review All");
|
||||
|
||||
await page.getByTestId("review-button").first().click();
|
||||
|
||||
await page.waitForSelector("button[data-testid='backup-flow-checkbox']", {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
expect(await page.getByTestId("backup-flow-checkbox").isChecked()).toBe(true);
|
||||
|
||||
await page.getByRole("button", { name: "Update Component" }).click();
|
||||
|
||||
await expect(page.getByTestId("update-button")).toHaveCount(1, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId("review-button")).toHaveCount(3, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await expect(page.getByText("Updates are available for 4")).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
expect(await page.getByTestId("update-all-button")).toHaveText("Review All");
|
||||
|
||||
await page.getByTestId("update-button").first().click();
|
||||
|
||||
await expect(page.getByTestId("update-button")).toHaveCount(0, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId("review-button")).toHaveCount(3, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await awaitBootstrapTest(page, { skipModal: true });
|
||||
|
||||
await expect(page.getByText("Backup").count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue