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,
|
showNode,
|
||||||
data,
|
data,
|
||||||
buildStatus,
|
buildStatus,
|
||||||
|
dismissAll,
|
||||||
isOutdated,
|
isOutdated,
|
||||||
isUserEdited,
|
isUserEdited,
|
||||||
|
isBreakingChange,
|
||||||
getValidationStatus,
|
getValidationStatus,
|
||||||
handleUpdateComponent,
|
|
||||||
}: {
|
}: {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
|
|
@ -52,10 +53,11 @@ export default function NodeStatus({
|
||||||
showNode: boolean;
|
showNode: boolean;
|
||||||
data: NodeDataType;
|
data: NodeDataType;
|
||||||
buildStatus: BuildStatus;
|
buildStatus: BuildStatus;
|
||||||
|
dismissAll: boolean;
|
||||||
isOutdated: boolean;
|
isOutdated: boolean;
|
||||||
isUserEdited: boolean;
|
isUserEdited: boolean;
|
||||||
|
isBreakingChange: boolean;
|
||||||
getValidationStatus: (data) => VertexBuildTypeAPI | null;
|
getValidationStatus: (data) => VertexBuildTypeAPI | null;
|
||||||
handleUpdateComponent: () => void;
|
|
||||||
}) {
|
}) {
|
||||||
const nodeId_ = data.node?.flow?.data
|
const nodeId_ = data.node?.flow?.data
|
||||||
? (findLastNode(data.node?.flow.data!)?.id ?? nodeId)
|
? (findLastNode(data.node?.flow.data!)?.id ?? nodeId)
|
||||||
|
|
@ -182,8 +184,6 @@ export default function NodeStatus({
|
||||||
getValidationStatus,
|
getValidationStatus,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dismissAll = useUtilityStore((state) => state.dismissAll);
|
|
||||||
|
|
||||||
const getBaseBorderClass = (selected) => {
|
const getBaseBorderClass = (selected) => {
|
||||||
let className =
|
let className =
|
||||||
selected && !isBuilding
|
selected && !isBuilding
|
||||||
|
|
@ -191,8 +191,8 @@ export default function NodeStatus({
|
||||||
: "border ring-[0.5px] hover:shadow-node ring-border";
|
: "border ring-[0.5px] hover:shadow-node ring-border";
|
||||||
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
|
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
|
||||||
let updateClass =
|
let updateClass =
|
||||||
isOutdated && !isUserEdited && !dismissAll
|
isOutdated && !isUserEdited && !dismissAll && isBreakingChange
|
||||||
? "border-warning ring-2 ring-warning"
|
? "border-warning"
|
||||||
: "";
|
: "";
|
||||||
return cn(frozen ? frozenClass : className, updateClass);
|
return cn(frozen ? frozenClass : className, updateClass);
|
||||||
};
|
};
|
||||||
|
|
@ -464,36 +464,6 @@ export default function NodeStatus({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ShadTooltip>
|
</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>
|
</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 ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
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 { useAlternate } from "@/shared/hooks/use-alternate";
|
||||||
import { useUtilityStore } from "@/stores/utilityStore";
|
import { FlowStoreType } from "@/types/zustand/flow";
|
||||||
import { useUpdateNodeInternals } from "@xyflow/react";
|
import { useUpdateNodeInternals } from "@xyflow/react";
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import {
|
import {
|
||||||
ICON_STROKE_WIDTH,
|
ICON_STROKE_WIDTH,
|
||||||
|
|
@ -24,12 +26,12 @@ import { NodeDataType } from "../../types/flow";
|
||||||
import { checkHasToolMode } from "../../utils/reactflowUtils";
|
import { checkHasToolMode } from "../../utils/reactflowUtils";
|
||||||
import { classNames, cn } from "../../utils/utils";
|
import { classNames, cn } from "../../utils/utils";
|
||||||
import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields";
|
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 useUpdateNodeCode from "../hooks/use-update-node-code";
|
||||||
import NodeDescription from "./components/NodeDescription";
|
import NodeDescription from "./components/NodeDescription";
|
||||||
import NodeName from "./components/NodeName";
|
import NodeName from "./components/NodeName";
|
||||||
import { OutputParameter } from "./components/NodeOutputParameter";
|
import { OutputParameter } from "./components/NodeOutputParameter";
|
||||||
import NodeStatus from "./components/NodeStatus";
|
import NodeStatus from "./components/NodeStatus";
|
||||||
|
import NodeUpdateComponent from "./components/NodeUpdateComponent";
|
||||||
import RenderInputParameters from "./components/RenderInputParameters";
|
import RenderInputParameters from "./components/RenderInputParameters";
|
||||||
import { NodeIcon } from "./components/nodeIcon";
|
import { NodeIcon } from "./components/nodeIcon";
|
||||||
import { useBuildStatus } from "./hooks/use-get-build-status";
|
import { useBuildStatus } from "./hooks/use-get-build-status";
|
||||||
|
|
@ -72,13 +74,12 @@ function GenericNode({
|
||||||
xPos?: number;
|
xPos?: number;
|
||||||
yPos?: number;
|
yPos?: number;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [isOutdated, setIsOutdated] = useState(false);
|
|
||||||
const [isUserEdited, setIsUserEdited] = useState(false);
|
|
||||||
const [borderColor, setBorderColor] = useState<string>("");
|
const [borderColor, setBorderColor] = useState<string>("");
|
||||||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
||||||
const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
|
const [showHiddenOutputs, setShowHiddenOutputs] = useState(false);
|
||||||
const [validationStatus, setValidationStatus] =
|
const [validationStatus, setValidationStatus] =
|
||||||
useState<VertexBuildTypeAPI | null>(null);
|
useState<VertexBuildTypeAPI | null>(null);
|
||||||
|
const [openUpdateModal, setOpenUpdateModal] = useState(false);
|
||||||
|
|
||||||
const types = useTypesStore((state) => state.types);
|
const types = useTypesStore((state) => state.types);
|
||||||
const templates = useTypesStore((state) => state.templates);
|
const templates = useTypesStore((state) => state.templates);
|
||||||
|
|
@ -90,7 +91,15 @@ function GenericNode({
|
||||||
const edges = useFlowStore((state) => state.edges);
|
const edges = useFlowStore((state) => state.edges);
|
||||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||||
const buildStatus = useBuildStatus(data, data.id);
|
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;
|
const showNode = data.showNode ?? true;
|
||||||
|
|
||||||
|
|
@ -104,16 +113,31 @@ function GenericNode({
|
||||||
const [editNameDescription, toggleEditNameDescription, set] =
|
const [editNameDescription, toggleEditNameDescription, set] =
|
||||||
useAlternate(false);
|
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(
|
const updateNodeCode = useUpdateNodeCode(
|
||||||
data?.id,
|
data?.id,
|
||||||
data.node!,
|
data.node!,
|
||||||
setNode,
|
setNode,
|
||||||
setIsOutdated,
|
|
||||||
setIsUserEdited,
|
|
||||||
updateNodeInternals,
|
updateNodeInternals,
|
||||||
);
|
);
|
||||||
|
|
||||||
useCheckCodeValidity(data, templates, setIsOutdated, setIsUserEdited, types);
|
useEffect(() => {
|
||||||
|
updateNodeInternals(data.id);
|
||||||
|
}, [data.node.template]);
|
||||||
|
|
||||||
if (!data.node!.template) {
|
if (!data.node!.template) {
|
||||||
setErrorData({
|
setErrorData({
|
||||||
|
|
@ -127,7 +151,12 @@ function GenericNode({
|
||||||
deleteNode(data.id);
|
deleteNode(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateCode = useCallback(() => {
|
const handleUpdateCode = useCallback(
|
||||||
|
(confirmed: boolean = false) => {
|
||||||
|
if (!confirmed && hasBreakingChange) {
|
||||||
|
setOpenUpdateModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoadingUpdate(true);
|
setLoadingUpdate(true);
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
|
|
||||||
|
|
@ -147,6 +176,7 @@ function GenericNode({
|
||||||
data.id,
|
data.id,
|
||||||
);
|
);
|
||||||
updateNodeCode(newNode, currentCode, "code", type);
|
updateNodeCode(newNode, currentCode, "code", type);
|
||||||
|
removeDismissedNodes([data.id]);
|
||||||
setLoadingUpdate(false);
|
setLoadingUpdate(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -164,15 +194,18 @@ function GenericNode({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
},
|
||||||
|
[
|
||||||
data,
|
data,
|
||||||
templates,
|
templates,
|
||||||
|
hasBreakingChange,
|
||||||
edges,
|
edges,
|
||||||
updateNodeCode,
|
updateNodeCode,
|
||||||
validateComponentCode,
|
validateComponentCode,
|
||||||
setErrorData,
|
setErrorData,
|
||||||
takeSnapshot,
|
takeSnapshot,
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const handleUpdateCodeWShortcut = useCallback(() => {
|
const handleUpdateCodeWShortcut = useCallback(() => {
|
||||||
if (isOutdated && selected) {
|
if (isOutdated && selected) {
|
||||||
|
|
@ -194,11 +227,6 @@ function GenericNode({
|
||||||
[data.node?.outputs, data.node?.tool_mode],
|
[data.node?.outputs, data.node?.tool_mode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasToolMode = useMemo(
|
|
||||||
() => checkHasToolMode(data.node?.template ?? {}),
|
|
||||||
[data.node?.template],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasOutputs = useMemo(
|
const hasOutputs = useMemo(
|
||||||
() => data.node?.outputs && data.node.outputs.length > 0,
|
() => data.node?.outputs && data.node.outputs.length > 0,
|
||||||
[data.node?.outputs],
|
[data.node?.outputs],
|
||||||
|
|
@ -270,6 +298,11 @@ function GenericNode({
|
||||||
return useFlowStore.getState().nodes.filter((node) => node.selected).length;
|
return useFlowStore.getState().nodes.filter((node) => node.selected).length;
|
||||||
}, [selected]);
|
}, [selected]);
|
||||||
|
|
||||||
|
const shouldShowUpdateComponent = useMemo(
|
||||||
|
() => (isOutdated || hasBreakingChange) && !isUserEdited && !dismissAll,
|
||||||
|
[isOutdated, hasBreakingChange, isUserEdited, dismissAll],
|
||||||
|
);
|
||||||
|
|
||||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||||
return selected && selectedNodesCount === 1 ? (
|
return selected && selectedNodesCount === 1 ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -295,8 +328,10 @@ function GenericNode({
|
||||||
showNode={showNode}
|
showNode={showNode}
|
||||||
openAdvancedModal={false}
|
openAdvancedModal={false}
|
||||||
onCloseAdvancedModal={() => {}}
|
onCloseAdvancedModal={() => {}}
|
||||||
updateNode={handleUpdateCode}
|
updateNode={() => handleUpdateCode()}
|
||||||
isOutdated={isOutdated && isUserEdited}
|
isOutdated={isOutdated && dismissAll}
|
||||||
|
isUserEdited={isUserEdited}
|
||||||
|
hasBreakingChange={hasBreakingChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="-z-10">
|
<div className="-z-10">
|
||||||
|
|
@ -410,10 +445,11 @@ function GenericNode({
|
||||||
selected={selected}
|
selected={selected}
|
||||||
setBorderColor={setBorderColor}
|
setBorderColor={setBorderColor}
|
||||||
buildStatus={buildStatus}
|
buildStatus={buildStatus}
|
||||||
|
dismissAll={dismissAll}
|
||||||
isOutdated={isOutdated}
|
isOutdated={isOutdated}
|
||||||
isUserEdited={isUserEdited}
|
isUserEdited={isUserEdited}
|
||||||
|
isBreakingChange={hasBreakingChange}
|
||||||
getValidationStatus={getValidationStatus}
|
getValidationStatus={getValidationStatus}
|
||||||
handleUpdateComponent={handleUpdateCode}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -463,42 +499,30 @@ function GenericNode({
|
||||||
}, [data, types, isToolMode, showNode, shownOutputs, showHiddenOutputs]);
|
}, [data, types, isToolMode, showNode, shownOutputs, showHiddenOutputs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn(shouldShowUpdateComponent ? "relative -mt-10" : "")}>
|
||||||
className={cn(
|
|
||||||
isOutdated && !isUserEdited && !dismissAll ? "relative -mt-10" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
borderColor,
|
borderColor,
|
||||||
showNode ? "w-80" : `w-48`,
|
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",
|
!hasOutputs && "pb-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{memoizedNodeToolbarComponent}
|
<UpdateComponentModal
|
||||||
{isOutdated && !isUserEdited && !dismissAll && (
|
open={openUpdateModal}
|
||||||
<div className="flex h-10 w-full items-center gap-4 rounded-t-[0.69rem] bg-warning p-2 px-4 text-warning-foreground">
|
setOpen={setOpenUpdateModal}
|
||||||
<ForwardedIconComponent
|
onUpdateNode={() => handleUpdateCode(true)}
|
||||||
name="AlertTriangle"
|
components={componentUpdate ? [componentUpdate] : []}
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
/>
|
||||||
className="icon-size shrink-0"
|
{memoizedNodeToolbarComponent}
|
||||||
|
{shouldShowUpdateComponent && (
|
||||||
|
<NodeUpdateComponent
|
||||||
|
hasBreakingChange={hasBreakingChange}
|
||||||
|
showNode={showNode}
|
||||||
|
handleUpdateCode={() => handleUpdateCode()}
|
||||||
|
loadingUpdate={loadingUpdate}
|
||||||
|
setDismissAll={() => addDismissedNodes([data.id])}
|
||||||
/>
|
/>
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
data-testid={`${data.id}-main-node`}
|
data-testid={`${data.id}-main-node`}
|
||||||
|
|
@ -529,8 +553,7 @@ function GenericNode({
|
||||||
{!showNode && (
|
{!showNode && (
|
||||||
<>
|
<>
|
||||||
{renderInputParameters()}
|
{renderInputParameters()}
|
||||||
{shownOutputs &&
|
{shownOutputs.length > 0 &&
|
||||||
shownOutputs.length > 0 &&
|
|
||||||
renderOutputs(shownOutputs, "render-outputs")}
|
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,
|
dataId: string,
|
||||||
dataNode: APIClassType, // Define YourNodeType according to your data structure
|
dataNode: APIClassType, // Define YourNodeType according to your data structure
|
||||||
setNode: (id: string, callback: (oldNode) => any) => void,
|
setNode: (id: string, callback: (oldNode) => any) => void,
|
||||||
setIsOutdated: (value: boolean) => void,
|
|
||||||
setIsUserEdited: (value: boolean) => void,
|
|
||||||
updateNodeInternals: (id: string) => void,
|
updateNodeInternals: (id: string) => void,
|
||||||
) => {
|
) => {
|
||||||
const { setComponentsToUpdate } = useFlowStore();
|
const { setComponentsToUpdate } = useFlowStore();
|
||||||
|
|
@ -30,8 +28,6 @@ const useUpdateNodeCode = (
|
||||||
}
|
}
|
||||||
|
|
||||||
newNode.data.node.template[name].value = code;
|
newNode.data.node.template[name].value = code;
|
||||||
setIsOutdated(false);
|
|
||||||
setIsUserEdited(false);
|
|
||||||
|
|
||||||
const outputs = dataNode.outputs;
|
const outputs = dataNode.outputs;
|
||||||
const updatedOutputs = newNodeClass.outputs;
|
const updatedOutputs = newNodeClass.outputs;
|
||||||
|
|
@ -44,10 +40,12 @@ const useUpdateNodeCode = (
|
||||||
return newNode;
|
return newNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
setComponentsToUpdate((old) => old.filter((id) => id !== dataId));
|
setComponentsToUpdate((old) =>
|
||||||
|
old.filter((component) => component.id !== dataId),
|
||||||
|
);
|
||||||
updateNodeInternals(dataId);
|
updateNodeInternals(dataId);
|
||||||
},
|
},
|
||||||
[dataId, dataNode, setNode, setIsOutdated, updateNodeInternals],
|
[dataId, dataNode, setNode, updateNodeInternals],
|
||||||
);
|
);
|
||||||
|
|
||||||
return updateNodeCode;
|
return updateNodeCode;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { CustomProductSelector } from "@/customization/components/custom-product
|
||||||
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
|
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
|
||||||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||||
import useTheme from "@/customization/hooks/use-custom-theme";
|
import useTheme from "@/customization/hooks/use-custom-theme";
|
||||||
import { useResetDismissUpdateAll } from "@/hooks/use-reset-dismiss-update-all";
|
|
||||||
import useAlertStore from "@/stores/alertStore";
|
import useAlertStore from "@/stores/alertStore";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { AccountMenu } from "./components/AccountMenu";
|
import { AccountMenu } from "./components/AccountMenu";
|
||||||
|
|
@ -43,8 +42,6 @@ export default function AppHeader(): JSX.Element {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useResetDismissUpdateAll();
|
|
||||||
|
|
||||||
const getNotificationBadge = () => {
|
const getNotificationBadge = () => {
|
||||||
const baseClasses = "absolute h-1 w-1 rounded-full bg-destructive";
|
const baseClasses = "absolute h-1 w-1 rounded-full bg-destructive";
|
||||||
return notificationCenter
|
return notificationCenter
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ const useAddFlow = () => {
|
||||||
const setFlows = useFlowsManagerStore((state) => state.setFlows);
|
const setFlows = useFlowsManagerStore((state) => state.setFlows);
|
||||||
const { deleteFlow } = useDeleteFlow();
|
const { deleteFlow } = useDeleteFlow();
|
||||||
|
|
||||||
const { setFlowToCanvas } = useFlowsManagerStore();
|
|
||||||
const setNoticeData = useAlertStore.getState().setNoticeData;
|
const setNoticeData = useAlertStore.getState().setNoticeData;
|
||||||
const { folderId } = useParams();
|
const { folderId } = useParams();
|
||||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||||
|
|
@ -93,7 +92,6 @@ const useAddFlow = () => {
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setFlowToCanvas(createdFlow);
|
|
||||||
resolve(createdFlow.id);
|
resolve(createdFlow.id);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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]";
|
minWidth = "min-w-[40vw]";
|
||||||
height = "";
|
height = "";
|
||||||
break;
|
break;
|
||||||
|
case "small-update":
|
||||||
|
minWidth = "min-w-[480px] max-w-[480px]";
|
||||||
|
height = "";
|
||||||
|
break;
|
||||||
case "small":
|
case "small":
|
||||||
minWidth = "min-w-[40vw]";
|
minWidth = "min-w-[40vw]";
|
||||||
height = "h-[40vh]";
|
height = "h-[40vh]";
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,7 @@ interface BaseModalProps {
|
||||||
| "retangular"
|
| "retangular"
|
||||||
| "smaller"
|
| "smaller"
|
||||||
| "small"
|
| "small"
|
||||||
|
| "small-update"
|
||||||
| "small-query"
|
| "small-query"
|
||||||
| "medium"
|
| "medium"
|
||||||
| "medium-tall"
|
| "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 { Button } from "@/components/ui/button";
|
||||||
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code";
|
||||||
import { processNodeAdvancedFields } from "@/CustomNodes/helpers/process-node-advanced-fields";
|
import { processNodeAdvancedFields } from "@/CustomNodes/helpers/process-node-advanced-fields";
|
||||||
import useUpdateAllNodes, {
|
import useUpdateAllNodes, {
|
||||||
UpdateNodesType,
|
UpdateNodesType,
|
||||||
} from "@/CustomNodes/hooks/use-update-all-nodes";
|
} from "@/CustomNodes/hooks/use-update-all-nodes";
|
||||||
|
import UpdateComponentModal from "@/modals/updateComponentModal";
|
||||||
import useAlertStore from "@/stores/alertStore";
|
import useAlertStore from "@/stores/alertStore";
|
||||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||||
import useFlowStore from "@/stores/flowStore";
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import { useTypesStore } from "@/stores/typesStore";
|
import { useTypesStore } from "@/stores/typesStore";
|
||||||
import { useUtilityStore } from "@/stores/utilityStore";
|
|
||||||
import { cn } from "@/utils/utils";
|
import { cn } from "@/utils/utils";
|
||||||
import { useUpdateNodeInternals } from "@xyflow/react";
|
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 = "Error updating components";
|
||||||
const ERROR_MESSAGE_UPDATING_COMPONENTS_LIST = [
|
const ERROR_MESSAGE_UPDATING_COMPONENTS_LIST = [
|
||||||
|
|
@ -24,7 +23,6 @@ const ERROR_MESSAGE_EDGES_LOST =
|
||||||
|
|
||||||
export default function UpdateAllComponents({}: {}) {
|
export default function UpdateAllComponents({}: {}) {
|
||||||
const { componentsToUpdate, nodes, edges, setNodes } = useFlowStore();
|
const { componentsToUpdate, nodes, edges, setNodes } = useFlowStore();
|
||||||
const setDismissAll = useUtilityStore((state) => state.setDismissAll);
|
|
||||||
const templates = useTypesStore((state) => state.templates);
|
const templates = useTypesStore((state) => state.templates);
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
const { mutateAsync: validateComponentCode } = usePostValidateComponentCode();
|
const { mutateAsync: validateComponentCode } = usePostValidateComponentCode();
|
||||||
|
|
@ -34,7 +32,26 @@ export default function UpdateAllComponents({}: {}) {
|
||||||
const updateAllNodes = useUpdateAllNodes(setNodes, updateNodeInternals);
|
const updateAllNodes = useUpdateAllNodes(setNodes, updateNodeInternals);
|
||||||
|
|
||||||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
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({
|
const edgesUpdateRef = useRef({
|
||||||
numberOfEdgesBeforeUpdate: 0,
|
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();
|
startEdgesUpdateRef();
|
||||||
|
|
||||||
setLoadingUpdate(true);
|
setLoadingUpdate(true);
|
||||||
|
|
@ -71,8 +96,10 @@ export default function UpdateAllComponents({}: {}) {
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
const updates: UpdateNodesType[] = [];
|
const updates: UpdateNodesType[] = [];
|
||||||
|
|
||||||
const updatePromises = componentsToUpdate.map((nodeId) => {
|
const updatePromises = componentsToUpdateFiltered
|
||||||
const node = nodes.find((n) => n.id === nodeId);
|
.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();
|
if (!node || node.type !== "genericNode") return Promise.resolve();
|
||||||
|
|
||||||
const thisNodeTemplate = templates[node.data.type]?.template;
|
const thisNodeTemplate = templates[node.data.type]?.template;
|
||||||
|
|
@ -87,10 +114,14 @@ export default function UpdateAllComponents({}: {}) {
|
||||||
})
|
})
|
||||||
.then(({ data: resData, type }) => {
|
.then(({ data: resData, type }) => {
|
||||||
if (resData && type) {
|
if (resData && type) {
|
||||||
const newNode = processNodeAdvancedFields(resData, edges, nodeId);
|
const newNode = processNodeAdvancedFields(
|
||||||
|
resData,
|
||||||
|
edges,
|
||||||
|
nodeUpdate.id,
|
||||||
|
);
|
||||||
|
|
||||||
updates.push({
|
updates.push({
|
||||||
nodeId,
|
nodeId: nodeUpdate.id,
|
||||||
newNode,
|
newNode,
|
||||||
code: currentCode,
|
code: currentCode,
|
||||||
name: "code",
|
name: "code",
|
||||||
|
|
@ -144,50 +175,59 @@ export default function UpdateAllComponents({}: {}) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (componentsToUpdate.length === 0) return null;
|
if (componentsToUpdateFiltered.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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%]",
|
dismissed && "translate-y-[120%]",
|
||||||
|
componentsToUpdateFiltered.some(
|
||||||
|
(component) => component.breakingChange,
|
||||||
|
) && "border-accent-amber-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<ForwardedIconComponent
|
|
||||||
name="AlertTriangle"
|
|
||||||
className="!h-[18px] !w-[18px] shrink-0"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
<span>
|
<span>
|
||||||
{componentsToUpdate.length} component
|
Update
|
||||||
{componentsToUpdate.length > 1 ? "s are" : " is"} ready to update
|
{componentsToUpdateFiltered.length > 1 ? "s are" : " is"} available
|
||||||
|
for{" "}
|
||||||
|
{componentsToUpdateFiltered.length +
|
||||||
|
" component" +
|
||||||
|
(componentsToUpdateFiltered.length > 1 ? "s" : "")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="shrink-0 text-sm text-warning-foreground"
|
className="shrink-0 text-sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
setDismissed(true);
|
addDismissedNodes(
|
||||||
setDismissAll(true);
|
componentsToUpdateFiltered.map((component) => component.id),
|
||||||
|
);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Dismiss
|
Dismiss {componentsToUpdateFiltered.length > 1 ? "All" : ""}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="warning"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
onClick={handleUpdateAllComponents}
|
onClick={() => handleUpdateAllComponents()}
|
||||||
loading={loadingUpdate}
|
loading={loadingUpdate}
|
||||||
data-testid="update-all-button"
|
data-testid="update-all-button"
|
||||||
>
|
>
|
||||||
Update All
|
{breakingChanges.length > 0 ? "Review All" : "Update All"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<UpdateComponentModal
|
||||||
|
isMultiple={true}
|
||||||
|
open={isOpen}
|
||||||
|
setOpen={setIsOpen}
|
||||||
|
onUpdateNode={(ids) => handleUpdateAllComponents(true, ids)}
|
||||||
|
components={componentsToUpdateFiltered}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ const NodeToolbarComponent = memo(
|
||||||
onCloseAdvancedModal,
|
onCloseAdvancedModal,
|
||||||
updateNode,
|
updateNode,
|
||||||
isOutdated,
|
isOutdated,
|
||||||
|
isUserEdited,
|
||||||
|
hasBreakingChange,
|
||||||
setOpenShowMoreOptions,
|
setOpenShowMoreOptions,
|
||||||
}: nodeToolbarPropsType): JSX.Element => {
|
}: nodeToolbarPropsType): JSX.Element => {
|
||||||
const version = useDarkStore((state) => state.version);
|
const version = useDarkStore((state) => state.version);
|
||||||
|
|
@ -102,8 +104,6 @@ const NodeToolbarComponent = memo(
|
||||||
Object.values(flow).includes(data.node?.display_name!),
|
Object.values(flow).includes(data.node?.display_name!),
|
||||||
);
|
);
|
||||||
|
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
|
||||||
|
|
||||||
const nodeLength = useMemo(() => getNodeLength(data), [data]);
|
const nodeLength = useMemo(() => getNodeLength(data), [data]);
|
||||||
const hasCode = useMemo(
|
const hasCode = useMemo(
|
||||||
() => Object.keys(data.node!.template).includes("code"),
|
() => Object.keys(data.node!.template).includes("code"),
|
||||||
|
|
@ -606,8 +606,9 @@ const NodeToolbarComponent = memo(
|
||||||
shortcuts.find((obj) => obj.name === "Update")
|
shortcuts.find((obj) => obj.name === "Update")
|
||||||
?.shortcut!
|
?.shortcut!
|
||||||
}
|
}
|
||||||
value={"Restore"}
|
style={hasBreakingChange ? "text-warning" : ""}
|
||||||
icon={"RefreshCcwDot"}
|
value={isUserEdited ? "Restore" : "Update"}
|
||||||
|
icon={isUserEdited ? "RefreshCcwDot" : "CircleArrowUp"}
|
||||||
dataTestId="update-button-modal"
|
dataTestId="update-button-modal"
|
||||||
/>
|
/>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,6 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
||||||
const flows = useFlowsManagerStore((state) => state.flows);
|
const flows = useFlowsManagerStore((state) => state.flows);
|
||||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||||
|
|
||||||
const flowToCanvas = useFlowsManagerStore((state) => state.flowToCanvas);
|
|
||||||
|
|
||||||
const updatedAt = currentSavedFlow?.updated_at;
|
const updatedAt = currentSavedFlow?.updated_at;
|
||||||
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
|
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
|
||||||
const stopBuilding = useFlowStore((state) => state.stopBuilding);
|
const stopBuilding = useFlowStore((state) => state.stopBuilding);
|
||||||
|
|
@ -112,19 +110,18 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
||||||
|
|
||||||
const isAnExistingFlowId = isAnExistingFlow.id;
|
const isAnExistingFlowId = isAnExistingFlow.id;
|
||||||
|
|
||||||
flowToCanvas
|
await getFlowToAddToCanvas(isAnExistingFlowId);
|
||||||
? setCurrentFlow(flowToCanvas)
|
|
||||||
: getFlowToAddToCanvas(isAnExistingFlowId);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
awaitgetTypes();
|
awaitgetTypes();
|
||||||
}, [id, flows, currentFlowId, flowToCanvas]);
|
}, [id, flows, currentFlowId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOnFlowPage(true);
|
setOnFlowPage(true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setOnFlowPage(false);
|
setOnFlowPage(false);
|
||||||
|
console.log("unmounting");
|
||||||
setCurrentFlow(undefined);
|
setCurrentFlow(undefined);
|
||||||
};
|
};
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
@ -152,6 +149,7 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
||||||
|
|
||||||
const getFlowToAddToCanvas = async (id: string) => {
|
const getFlowToAddToCanvas = async (id: string) => {
|
||||||
const flow = await getFlow({ id: id });
|
const flow = await getFlow({ id: id });
|
||||||
|
console.log(flow);
|
||||||
setCurrentFlow(flow);
|
setCurrentFlow(flow);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
import useAlertStore from "@/stores/alertStore";
|
import useAlertStore from "@/stores/alertStore";
|
||||||
import { FlowType } from "@/types/flow";
|
import { FlowType } from "@/types/flow";
|
||||||
import { downloadFlow } from "@/utils/reactflowUtils";
|
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";
|
import useSelectOptionsChange from "../../hooks/use-select-options-change";
|
||||||
|
|
||||||
type DropdownComponentProps = {
|
type DropdownComponentProps = {
|
||||||
|
|
@ -21,11 +21,15 @@ const DropdownComponent = ({
|
||||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
|
||||||
const { handleDuplicate } = useDuplicateFlows({
|
const { handleDuplicate } = useDuplicateFlow({ flow: flowData });
|
||||||
selectedFlowsComponentsCards: [flowData.id],
|
|
||||||
allFlows: [flowData],
|
const duplicateFlow = () => {
|
||||||
setSuccessData,
|
handleDuplicate().then(() =>
|
||||||
});
|
setSuccessData({
|
||||||
|
title: `${flowData.is_component ? "Component" : "Flow"} duplicated successfully`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
downloadFlow(flowData, flowData.name, flowData.description);
|
downloadFlow(flowData, flowData.name, flowData.description);
|
||||||
|
|
@ -35,7 +39,7 @@ const DropdownComponent = ({
|
||||||
[flowData.id],
|
[flowData.id],
|
||||||
setErrorData,
|
setErrorData,
|
||||||
setOpenDelete,
|
setOpenDelete,
|
||||||
handleDuplicate,
|
duplicateFlow,
|
||||||
handleExport,
|
handleExport,
|
||||||
handleEdit,
|
handleEdit,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,6 @@ const GridComponent = ({ flowData }: { flowData: FlowType }) => {
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
const { folderId } = useParams();
|
const { folderId } = useParams();
|
||||||
const isComponent = flowData.is_component ?? false;
|
const isComponent = flowData.is_component ?? false;
|
||||||
const setFlowToCanvas = useFlowsManagerStore(
|
|
||||||
(state) => state.setFlowToCanvas,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { getIcon } = useGetTemplateStyle(flowData);
|
const { getIcon } = useGetTemplateStyle(flowData);
|
||||||
|
|
||||||
|
|
@ -50,7 +47,6 @@ const GridComponent = ({ flowData }: { flowData: FlowType }) => {
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
if (!isComponent) {
|
if (!isComponent) {
|
||||||
await setFlowToCanvas(flowData);
|
|
||||||
navigate(editFlowLink);
|
navigate(editFlowLink);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,13 @@ const ListComponent = ({ flowData }: { flowData: FlowType }) => {
|
||||||
const { folderId } = useParams();
|
const { folderId } = useParams();
|
||||||
const [openSettings, setOpenSettings] = useState(false);
|
const [openSettings, setOpenSettings] = useState(false);
|
||||||
const isComponent = flowData.is_component ?? false;
|
const isComponent = flowData.is_component ?? false;
|
||||||
const setFlowToCanvas = useFlowsManagerStore(
|
|
||||||
(state) => state.setFlowToCanvas,
|
|
||||||
);
|
|
||||||
const { getIcon } = useGetTemplateStyle(flowData);
|
const { getIcon } = useGetTemplateStyle(flowData);
|
||||||
|
|
||||||
const editFlowLink = `/flow/${flowData.id}${folderId ? `/folder/${folderId}` : ""}`;
|
const editFlowLink = `/flow/${flowData.id}${folderId ? `/folder/${folderId}` : ""}`;
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
if (!isComponent) {
|
if (!isComponent) {
|
||||||
await setFlowToCanvas(flowData);
|
|
||||||
navigate(editFlowLink);
|
navigate(editFlowLink);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,31 @@
|
||||||
import { usePostAddFlow } from "@/controllers/API/queries/flows/use-post-add-flow";
|
import { usePostAddFlow } from "@/controllers/API/queries/flows/use-post-add-flow";
|
||||||
import { useFolderStore } from "@/stores/foldersStore";
|
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";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
type UseDuplicateFlowsParams = {
|
type UseDuplicateFlowsParams = {
|
||||||
selectedFlowsComponentsCards: string[];
|
flow?: FlowType;
|
||||||
allFlows: any[];
|
|
||||||
setSuccessData: (data: { title: string }) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const useDuplicateFlows = ({
|
const useDuplicateFlow = ({ flow }: UseDuplicateFlowsParams) => {
|
||||||
selectedFlowsComponentsCards,
|
|
||||||
allFlows,
|
|
||||||
setSuccessData,
|
|
||||||
}: UseDuplicateFlowsParams) => {
|
|
||||||
const { mutateAsync: postAddFlow } = usePostAddFlow();
|
const { mutateAsync: postAddFlow } = usePostAddFlow();
|
||||||
const { folderId } = useParams();
|
const { folderId } = useParams();
|
||||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||||
|
|
||||||
const handleDuplicate = async () => {
|
const handleDuplicate = async () => {
|
||||||
selectedFlowsComponentsCards.map(async (selectedFlow) => {
|
if (flow?.data) {
|
||||||
const currentFlow = allFlows.find((flow) => flow.id === selectedFlow);
|
|
||||||
const folder_id = folderId ?? myCollectionId ?? "";
|
const folder_id = folderId ?? myCollectionId ?? "";
|
||||||
|
|
||||||
const flowsToCheckNames = allFlows?.filter(
|
const newFlow = createNewFlow(flow.data, folder_id, flow);
|
||||||
(f) => f.folder_id === folder_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newFlow = createNewFlow(currentFlow.data, folder_id, currentFlow);
|
|
||||||
|
|
||||||
const newName = addVersionToDuplicates(newFlow, flowsToCheckNames ?? []);
|
|
||||||
newFlow.name = newName;
|
|
||||||
newFlow.folder_id = folder_id;
|
newFlow.folder_id = folder_id;
|
||||||
|
|
||||||
await postAddFlow(newFlow);
|
await postAddFlow(newFlow);
|
||||||
setSuccessData({
|
}
|
||||||
title: `${newFlow.is_component ? "Component" : "Flow"} duplicated successfully`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { handleDuplicate };
|
return { handleDuplicate };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useDuplicateFlows;
|
export default useDuplicateFlow;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import {
|
import { BROKEN_EDGES_WARNING } from "@/constants/constants";
|
||||||
BROKEN_EDGES_WARNING,
|
|
||||||
componentsToIgnoreUpdate,
|
|
||||||
} from "@/constants/constants";
|
|
||||||
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
|
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
|
||||||
import {
|
import {
|
||||||
track,
|
track,
|
||||||
trackDataLoaded,
|
trackDataLoaded,
|
||||||
trackFlowBuild,
|
trackFlowBuild,
|
||||||
} from "@/customization/utils/analytics";
|
} from "@/customization/utils/analytics";
|
||||||
|
import { checkCodeValidity } from "@/CustomNodes/helpers/check-code-validity";
|
||||||
import { brokenEdgeMessage } from "@/utils/utils";
|
import { brokenEdgeMessage } from "@/utils/utils";
|
||||||
import {
|
import {
|
||||||
EdgeChange,
|
EdgeChange,
|
||||||
|
|
@ -33,7 +31,11 @@ import {
|
||||||
sourceHandleType,
|
sourceHandleType,
|
||||||
targetHandleType,
|
targetHandleType,
|
||||||
} from "../types/flow";
|
} from "../types/flow";
|
||||||
import { FlowStoreType, VertexLayerElementType } from "../types/zustand/flow";
|
import {
|
||||||
|
ComponentsToUpdateType,
|
||||||
|
FlowStoreType,
|
||||||
|
VertexLayerElementType,
|
||||||
|
} from "../types/zustand/flow";
|
||||||
import { buildFlowVerticesWithFallback } from "../utils/buildUtils";
|
import { buildFlowVerticesWithFallback } from "../utils/buildUtils";
|
||||||
import {
|
import {
|
||||||
buildPositionDictionary,
|
buildPositionDictionary,
|
||||||
|
|
@ -88,24 +90,22 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
set({ componentsToUpdate: newChange });
|
set({ componentsToUpdate: newChange });
|
||||||
},
|
},
|
||||||
updateComponentsToUpdate: (nodes) => {
|
updateComponentsToUpdate: (nodes) => {
|
||||||
let outdatedNodes: string[] = [];
|
let outdatedNodes: ComponentsToUpdateType[] = [];
|
||||||
const templates = useTypesStore.getState().templates;
|
const templates = useTypesStore.getState().templates;
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
nodes.forEach((node) => {
|
||||||
let node = nodes[i];
|
|
||||||
if (node.type === "genericNode") {
|
if (node.type === "genericNode") {
|
||||||
const currentCode = templates[node.data?.type]?.template?.code?.value;
|
const codeValidity = checkCodeValidity(node.data, templates);
|
||||||
const thisNodesCode = node.data?.node!.template?.code?.value;
|
if (codeValidity && codeValidity.outdated)
|
||||||
if (
|
outdatedNodes.push({
|
||||||
currentCode &&
|
id: node.id,
|
||||||
thisNodesCode &&
|
icon: node.data.node?.icon,
|
||||||
currentCode !== thisNodesCode &&
|
display_name: node.data.node?.display_name,
|
||||||
!node.data?.node?.edited &&
|
outdated: codeValidity.outdated,
|
||||||
!componentsToIgnoreUpdate.includes(node.data?.type)
|
breakingChange: codeValidity.breakingChange,
|
||||||
) {
|
userEdited: codeValidity.userEdited,
|
||||||
outdatedNodes.push(node.id);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
set({ componentsToUpdate: outdatedNodes });
|
set({ componentsToUpdate: outdatedNodes });
|
||||||
},
|
},
|
||||||
onFlowPage: false,
|
onFlowPage: false,
|
||||||
|
|
@ -223,6 +223,11 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
let newEdges = cleanEdges(nodes, edges);
|
let newEdges = cleanEdges(nodes, edges);
|
||||||
const { inputs, outputs } = getInputsAndOutputs(nodes);
|
const { inputs, outputs } = getInputsAndOutputs(nodes);
|
||||||
get().updateComponentsToUpdate(nodes);
|
get().updateComponentsToUpdate(nodes);
|
||||||
|
set({
|
||||||
|
dismissedNodes: JSON.parse(
|
||||||
|
localStorage.getItem(`dismiss_${flow?.id}`) ?? "[]",
|
||||||
|
) as string[],
|
||||||
|
});
|
||||||
unselectAllNodesEdges(nodes, edges);
|
unselectAllNodesEdges(nodes, edges);
|
||||||
set({
|
set({
|
||||||
nodes,
|
nodes,
|
||||||
|
|
@ -992,6 +997,27 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
componentsToUpdate: [],
|
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;
|
export default useFlowStore;
|
||||||
|
|
|
||||||
|
|
@ -130,19 +130,11 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
setSelectedFlowsComponentsCards: (selectedFlowsComponentsCards: string[]) => {
|
setSelectedFlowsComponentsCards: (selectedFlowsComponentsCards: string[]) => {
|
||||||
set({ selectedFlowsComponentsCards });
|
set({ selectedFlowsComponentsCards });
|
||||||
},
|
},
|
||||||
flowToCanvas: null,
|
|
||||||
setFlowToCanvas: async (flowToCanvas: FlowType | null) => {
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
set({ flowToCanvas });
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
resetStore: () => {
|
resetStore: () => {
|
||||||
set({
|
set({
|
||||||
flows: [],
|
flows: [],
|
||||||
currentFlow: undefined,
|
currentFlow: undefined,
|
||||||
currentFlowId: "",
|
currentFlowId: "",
|
||||||
flowToCanvas: null,
|
|
||||||
searchFlowsComponents: "",
|
searchFlowsComponents: "",
|
||||||
selectedFlowsComponentsCards: [],
|
selectedFlowsComponentsCards: [],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import { create } from "zustand";
|
||||||
export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
|
export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
|
||||||
clientId: "",
|
clientId: "",
|
||||||
setClientId: (clientId: string) => set({ clientId }),
|
setClientId: (clientId: string) => set({ clientId }),
|
||||||
dismissAll: false,
|
|
||||||
setDismissAll: (dismissAll: boolean) => set({ dismissAll }),
|
|
||||||
chatValueStore: "",
|
chatValueStore: "",
|
||||||
setChatValueStore: (value: string) => set({ chatValueStore: value }),
|
setChatValueStore: (value: string) => set({ chatValueStore: value }),
|
||||||
selectedItems: [],
|
selectedItems: [],
|
||||||
|
|
|
||||||
|
|
@ -135,10 +135,19 @@
|
||||||
padding: 0rem 1rem !important;
|
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;
|
background-color: hsl(var(--accent)) !important;
|
||||||
}
|
}
|
||||||
|
.ag-tool-mode.ag-no-selection .ag-row-selected::before {
|
||||||
.ag-tool-mode .ag-row-selected:not(.ag-row-focus)::before {
|
|
||||||
background-color: hsl(var(--background)) !important;
|
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>;
|
types: Array<string>;
|
||||||
selected?: string;
|
selected?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
method?: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
proxy?: OutputFieldProxyType;
|
proxy?: OutputFieldProxyType;
|
||||||
|
|
|
||||||
|
|
@ -583,6 +583,8 @@ export type nodeToolbarPropsType = {
|
||||||
openAdvancedModal?: boolean;
|
openAdvancedModal?: boolean;
|
||||||
onCloseAdvancedModal?: (close: boolean) => void;
|
onCloseAdvancedModal?: (close: boolean) => void;
|
||||||
isOutdated: boolean;
|
isOutdated: boolean;
|
||||||
|
isUserEdited: boolean;
|
||||||
|
hasBreakingChange: boolean;
|
||||||
updateNode: () => void;
|
updateNode: () => void;
|
||||||
closeToolbar?: () => void;
|
closeToolbar?: () => void;
|
||||||
setOpenShowMoreOptions?: (open: boolean) => void;
|
setOpenShowMoreOptions?: (open: boolean) => void;
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,19 @@ export type FlowPoolType = {
|
||||||
[key: string]: Array<VertexBuildTypeAPI>;
|
[key: string]: Array<VertexBuildTypeAPI>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ComponentsToUpdateType = {
|
||||||
|
id: string;
|
||||||
|
icon?: string;
|
||||||
|
display_name: string;
|
||||||
|
outdated: boolean;
|
||||||
|
breakingChange: boolean;
|
||||||
|
userEdited: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type FlowStoreType = {
|
export type FlowStoreType = {
|
||||||
|
dismissedNodes: string[];
|
||||||
|
addDismissedNodes: (dismissedNodes: string[]) => void;
|
||||||
|
removeDismissedNodes: (dismissedNodes: string[]) => void;
|
||||||
//key x, y
|
//key x, y
|
||||||
positionDictionary: { [key: number]: number };
|
positionDictionary: { [key: number]: number };
|
||||||
isPositionAvailable: (position: { x: number; y: number }) => boolean;
|
isPositionAvailable: (position: { x: number; y: number }) => boolean;
|
||||||
|
|
@ -61,9 +73,11 @@ export type FlowStoreType = {
|
||||||
}) => void;
|
}) => void;
|
||||||
fitViewNode: (nodeId: string) => void;
|
fitViewNode: (nodeId: string) => void;
|
||||||
autoSaveFlow: (() => void) | undefined;
|
autoSaveFlow: (() => void) | undefined;
|
||||||
componentsToUpdate: string[];
|
componentsToUpdate: ComponentsToUpdateType[];
|
||||||
setComponentsToUpdate: (
|
setComponentsToUpdate: (
|
||||||
update: string[] | ((oldState: string[]) => string[]),
|
update:
|
||||||
|
| ComponentsToUpdateType[]
|
||||||
|
| ((oldState: ComponentsToUpdateType[]) => ComponentsToUpdateType[]),
|
||||||
) => void;
|
) => void;
|
||||||
updateComponentsToUpdate: (nodes: AllNodeType[]) => void;
|
updateComponentsToUpdate: (nodes: AllNodeType[]) => void;
|
||||||
onFlowPage: boolean;
|
onFlowPage: boolean;
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ export type FlowsManagerStoreType = {
|
||||||
setAutoSavingInterval: (autoSavingInterval: number) => void;
|
setAutoSavingInterval: (autoSavingInterval: number) => void;
|
||||||
healthCheckMaxRetries: number;
|
healthCheckMaxRetries: number;
|
||||||
setHealthCheckMaxRetries: (healthCheckMaxRetries: number) => void;
|
setHealthCheckMaxRetries: (healthCheckMaxRetries: number) => void;
|
||||||
flowToCanvas: FlowType | null;
|
|
||||||
setFlowToCanvas: (flowToCanvas: FlowType | null) => Promise<void>;
|
|
||||||
IOModalOpen: boolean;
|
IOModalOpen: boolean;
|
||||||
setIOModalOpen: (IOModalOpen: boolean) => void;
|
setIOModalOpen: (IOModalOpen: boolean) => void;
|
||||||
resetStore: () => void;
|
resetStore: () => void;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ export type UtilityStoreType = {
|
||||||
playgroundScrollBehaves: ScrollBehavior;
|
playgroundScrollBehaves: ScrollBehavior;
|
||||||
setPlaygroundScrollBehaves: (behaves: ScrollBehavior) => void;
|
setPlaygroundScrollBehaves: (behaves: ScrollBehavior) => void;
|
||||||
maxFileSizeUpload: number;
|
maxFileSizeUpload: number;
|
||||||
setMaxFileSizeUpload: (maxFileSizeUpload: number) => void;
|
|
||||||
flowsPagination: Pagination;
|
flowsPagination: Pagination;
|
||||||
setFlowsPagination: (pagination: Pagination) => void;
|
setFlowsPagination: (pagination: Pagination) => void;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
|
|
@ -20,8 +19,6 @@ export type UtilityStoreType = {
|
||||||
setWebhookPollingInterval: (webhookPollingInterval: number) => void;
|
setWebhookPollingInterval: (webhookPollingInterval: number) => void;
|
||||||
chatValueStore: string;
|
chatValueStore: string;
|
||||||
setChatValueStore: (value: string) => void;
|
setChatValueStore: (value: string) => void;
|
||||||
dismissAll: boolean;
|
|
||||||
setDismissAll: (dismissAll: boolean) => void;
|
|
||||||
currentSessionId: string;
|
currentSessionId: string;
|
||||||
setCurrentSessionId: (sessionId: string) => void;
|
setCurrentSessionId: (sessionId: string) => void;
|
||||||
setClientId: (clientId: 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 { readFileSync } from "fs";
|
||||||
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
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 awaitBootstrapTest(page);
|
||||||
|
|
||||||
await page.locator("span").filter({ hasText: "Close" }).first().click();
|
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,
|
dataTransfer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
await page.waitForSelector("data-testid=list-card", {
|
await page.waitForSelector("data-testid=list-card", {
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId("list-card").first().click();
|
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,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
let outdatedComponents = await page.getByTestId("icon-AlertTriangle").count();
|
let outdatedComponents = await page.getByTestId("update-button").count();
|
||||||
expect(outdatedComponents).toBeGreaterThan(0);
|
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,
|
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