diff --git a/packages/react-ui/setup-tests.ts b/packages/react-ui/setup-tests.ts index a1ea062726..aabd227f72 100644 --- a/packages/react-ui/setup-tests.ts +++ b/packages/react-ui/setup-tests.ts @@ -1,2 +1,18 @@ import { TextDecoder, TextEncoder } from 'util'; Object.assign(global, { TextDecoder, TextEncoder }); + +jest.mock('react-markdown', () => ({ + __esModule: true, + default: jest.fn().mockImplementation(({ children }) => children), +})); + +jest.mock('@/app/lib/api', () => ({ + API_BASE_URL: 'http://localhost:3000', + api: { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + patch: jest.fn(), + }, +})); diff --git a/packages/react-ui/src/app/features/builder/builder-header/builder-header-action-bar.tsx b/packages/react-ui/src/app/features/builder/builder-header/builder-header-action-bar.tsx index b5be741137..6a60e3a4bb 100644 --- a/packages/react-ui/src/app/features/builder/builder-header/builder-header-action-bar.tsx +++ b/packages/react-ui/src/app/features/builder/builder-header/builder-header-action-bar.tsx @@ -1,4 +1,3 @@ -import { LeftSideBarType } from '@/app/features/builder/builder-hooks'; import { Button, cn, @@ -13,6 +12,7 @@ import { t } from 'i18next'; import { EllipsisVertical, History, Workflow } from 'lucide-react'; import { useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; +import { LeftSideBarType } from '../builder-types'; const ICON_SIZE_SMALL = 16; const ICON_SIZE_LARGE = 24; diff --git a/packages/react-ui/src/app/features/builder/builder-header/builder-header.tsx b/packages/react-ui/src/app/features/builder/builder-header/builder-header.tsx index 5ca1ca9237..80c615e42e 100644 --- a/packages/react-ui/src/app/features/builder/builder-header/builder-header.tsx +++ b/packages/react-ui/src/app/features/builder/builder-header/builder-header.tsx @@ -1,9 +1,7 @@ import { BuilderHeaderActionBar } from '@/app/features/builder/builder-header/builder-header-action-bar'; import { SideMenuCollapsed } from '@/app/features/builder/builder-header/side-menu-collapsed'; -import { - LeftSideBarType, - useBuilderStateContext, -} from '@/app/features/builder/builder-hooks'; +import { useBuilderStateContext } from '@/app/features/builder/builder-hooks'; +import { LeftSideBarType } from '../builder-types'; import { ExpandSideMenu } from '@/app/features/builder/builder-header/expand-side-menu'; import { WorkflowOverview } from '@/app/features/builder/builder-header/workflow-overview/workflow-overview'; diff --git a/packages/react-ui/src/app/features/builder/builder-header/builder-publish-button.tsx b/packages/react-ui/src/app/features/builder/builder-header/builder-publish-button.tsx index 4db757c23f..702b4dd2d6 100644 --- a/packages/react-ui/src/app/features/builder/builder-header/builder-publish-button.tsx +++ b/packages/react-ui/src/app/features/builder/builder-header/builder-publish-button.tsx @@ -10,10 +10,10 @@ import { FlowVersionState } from '@openops/shared'; import { t } from 'i18next'; import React from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; +import { LeftSideBarType } from '../builder-types'; import { SEARCH_PARAMS } from '@/app/constants/search-params'; import { - LeftSideBarType, useBuilderStateContext, useSwitchToDraft, } from '@/app/features/builder/builder-hooks'; diff --git a/packages/react-ui/src/app/features/builder/builder-hooks.ts b/packages/react-ui/src/app/features/builder/builder-hooks.ts index 2d2878c38c..19413fdbd7 100644 --- a/packages/react-ui/src/app/features/builder/builder-hooks.ts +++ b/packages/react-ui/src/app/features/builder/builder-hooks.ts @@ -1,6 +1,5 @@ import { AI_CHAT_CONTAINER_SIZES, - AiChatContainerSizeState, INTERNAL_ERROR_TOAST, toast, } from '@openops/components/ui'; @@ -10,11 +9,8 @@ import { create, StateCreator, useStore } from 'zustand'; import { devtools } from 'zustand/middleware'; import { flowsApi } from '@/app/features/flows/lib/flows-api'; -import { PromiseQueue } from '@/app/lib/promise-queue'; -import { BlockProperty } from '@openops/blocks-framework'; import { Flow, - flowHelper, FlowOperationRequest, FlowOperationType, FlowRun, @@ -24,10 +20,17 @@ import { TriggerType, } from '@openops/shared'; import { flowRunUtils } from '../flow-runs/lib/flow-run-utils'; -import { aiChatApi } from './ai-chat/lib/chat-api'; +import { + BuilderInitialState, + BuilderState, + InsertMentionHandler, + LeftSideBarType, + MidpanelAction, + MidpanelState, + RightSideBarType, +} from './builder-types'; import { DataSelectorSizeState } from './data-selector/data-selector-size-togglers'; - -const flowUpdatesQueue = new PromiseQueue(); +import { updateFlowVersion } from './update-flow-version'; export const BuilderStateContext = createContext(null); @@ -56,103 +59,6 @@ export function useSafeBuilderStateContext( return result; } -export enum LeftSideBarType { - RUNS = 'runs', - VERSIONS = 'versions', - RUN_DETAILS = 'run-details', - MENU = 'menu', - TREE_VIEW = 'tree-view', - NONE = 'none', -} - -export enum RightSideBarType { - NONE = 'none', - BLOCK_SETTINGS = 'block-settings', -} - -type InsertMentionHandler = (propertyPath: string) => void; - -export type MidpanelState = { - showDataSelector: boolean; - dataSelectorSize: DataSelectorSizeState; - showAiChat: boolean; - aiContainerSize: AiChatContainerSizeState; - aiChatProperty?: BlockProperty & { - inputName: `settings.input.${string}`; - }; - codeToInject?: string; -}; - -type MidpanelAction = - | { type: 'FOCUS_INPUT_WITH_MENTIONS' } - | { type: 'DATASELECTOR_MIMIZE_CLICK' } - | { type: 'DATASELECTOR_DOCK_CLICK' } - | { type: 'DATASELECTOR_EXPAND_CLICK' } - | { type: 'AICHAT_CLOSE_CLICK' } - | { type: 'AICHAT_MIMIZE_CLICK' } - | { type: 'AICHAT_DOCK_CLICK' } - | { type: 'AICHAT_EXPAND_CLICK' } - | { type: 'PANEL_CLICK_AWAY' } - | { - type: 'GENERATE_WITH_AI_CLICK'; - property?: BlockProperty & { inputName: `settings.input.${string}` }; - } - | { type: 'ADD_CODE_TO_INJECT'; code: string } - | { type: 'CLEAN_CODE_TO_INJECT' }; - -export type BuilderState = { - flow: Flow; - flowVersion: FlowVersion; - - readonly: boolean; - loopsIndexes: Record; - run: FlowRun | null; - leftSidebar: LeftSideBarType; - rightSidebar: RightSideBarType; - selectedStep: string | null; - canExitRun: boolean; - activeDraggingStep: string | null; - saving: boolean; - refreshBlockFormSettings: boolean; - refreshSettings: () => void; - exitRun: () => void; - exitStepSettings: () => void; - renameFlowClientSide: (newName: string) => void; - moveToFolderClientSide: (folderId: string) => void; - setRun: (run: FlowRun, flowVersion: FlowVersion) => void; - setLeftSidebar: (leftSidebar: LeftSideBarType) => void; - setRightSidebar: (rightSidebar: RightSideBarType) => void; - applyOperation: ( - operation: FlowOperationRequest, - onError: () => void, - ) => void; - removeStepSelection: () => void; - selectStepByName: (stepName: string, openRightSideBar?: boolean) => void; - startSaving: () => void; - setActiveDraggingStep: (stepName: string | null) => void; - setFlow: (flow: Flow) => void; - exitBlockSelector: () => void; - setVersion: (flowVersion: FlowVersion) => void; - setVersionUpdateTimestamp: (updateTimestamp: string) => void; - insertMention: InsertMentionHandler | null; - setReadOnly: (readOnly: boolean) => void; - setInsertMentionHandler: (handler: InsertMentionHandler | null) => void; - setLoopIndex: (stepName: string, index: number) => void; - canUndo: boolean; - setCanUndo: (canUndo: boolean) => void; - canRedo: boolean; - setCanRedo: (canUndo: boolean) => void; - dynamicPropertiesAuthReconnectCounter: number; - refreshDynamicPropertiesForAuth: () => void; - midpanelState: MidpanelState; - applyMidpanelAction: (midpanelAction: MidpanelAction) => void; -}; - -export type BuilderInitialState = Pick< - BuilderState, - 'flow' | 'flowVersion' | 'readonly' | 'run' | 'canExitRun' ->; - export type BuilderStore = ReturnType; export const createBuilderStore = (initialState: BuilderInitialState) => create( @@ -597,73 +503,3 @@ const applyMidpanelAction = (state: BuilderState, action: MidpanelAction) => { midpanelState: { ...state.midpanelState, ...newMidpanelState }, }; }; - -async function deleteChatRequest(flowVersion: FlowVersion, stepName: string) { - try { - const stepDetails = flowHelper.getStep(flowVersion, stepName); - const blockName = stepDetails?.settings?.blockName; - const chat = await aiChatApi.open(flowVersion.flowId, blockName, stepName); - await aiChatApi.delete(chat.chatId); - } catch (err) { - console.error(err); - } -} - -const updateFlowVersion = ( - state: BuilderState, - operation: FlowOperationRequest, - onError: () => void, - set: ( - partial: - | BuilderState - | Partial - | ((state: BuilderState) => BuilderState | Partial), - replace?: boolean | undefined, - ) => void, -) => { - const newFlowVersion = flowHelper.apply(state.flowVersion, operation); - if ( - operation.type === FlowOperationType.DELETE_ACTION && - operation.request.name === state.selectedStep - ) { - set({ selectedStep: undefined }); - set({ rightSidebar: RightSideBarType.NONE }); - deleteChatRequest(state.flowVersion, operation.request.name); - } - - if (operation.type === FlowOperationType.DUPLICATE_ACTION) { - set({ - selectedStep: flowHelper.getStep( - newFlowVersion, - operation.request.stepName, - )?.nextAction?.name, - }); - } - - const updateRequest = async () => { - set({ saving: true }); - try { - const updatedFlowVersion = await flowsApi.update( - state.flow.id, - operation, - ); - set((state) => { - return { - flowVersion: { - ...state.flowVersion, - id: updatedFlowVersion.version.id, - state: updatedFlowVersion.version.state, - updated: updatedFlowVersion.version.updated, - }, - saving: flowUpdatesQueue.size() !== 0, - }; - }); - } catch (error) { - console.error(error); - flowUpdatesQueue.halt(); - onError(); - } - }; - flowUpdatesQueue.add(updateRequest); - return { flowVersion: newFlowVersion }; -}; diff --git a/packages/react-ui/src/app/features/builder/builder-types.ts b/packages/react-ui/src/app/features/builder/builder-types.ts new file mode 100644 index 0000000000..0a727c48b1 --- /dev/null +++ b/packages/react-ui/src/app/features/builder/builder-types.ts @@ -0,0 +1,106 @@ +import { BlockProperty } from '@openops/blocks-framework'; +import { AiChatContainerSizeState } from '@openops/components/ui'; +import { + Flow, + FlowOperationRequest, + FlowRun, + FlowVersion, +} from '@openops/shared'; +import { DataSelectorSizeState } from './data-selector/data-selector-size-togglers'; + +export enum LeftSideBarType { + RUNS = 'runs', + VERSIONS = 'versions', + RUN_DETAILS = 'run-details', + MENU = 'menu', + TREE_VIEW = 'tree-view', + NONE = 'none', +} + +export enum RightSideBarType { + NONE = 'none', + BLOCK_SETTINGS = 'block-settings', +} + +export type InsertMentionHandler = (propertyPath: string) => void; + +export type MidpanelState = { + showDataSelector: boolean; + dataSelectorSize: DataSelectorSizeState; + showAiChat: boolean; + aiContainerSize: AiChatContainerSizeState; + aiChatProperty?: BlockProperty & { + inputName: `settings.input.${string}`; + }; + codeToInject?: string; +}; + +export type MidpanelAction = + | { type: 'FOCUS_INPUT_WITH_MENTIONS' } + | { type: 'DATASELECTOR_MIMIZE_CLICK' } + | { type: 'DATASELECTOR_DOCK_CLICK' } + | { type: 'DATASELECTOR_EXPAND_CLICK' } + | { type: 'AICHAT_CLOSE_CLICK' } + | { type: 'AICHAT_MIMIZE_CLICK' } + | { type: 'AICHAT_DOCK_CLICK' } + | { type: 'AICHAT_EXPAND_CLICK' } + | { type: 'PANEL_CLICK_AWAY' } + | { + type: 'GENERATE_WITH_AI_CLICK'; + property?: BlockProperty & { inputName: `settings.input.${string}` }; + } + | { type: 'ADD_CODE_TO_INJECT'; code: string } + | { type: 'CLEAN_CODE_TO_INJECT' }; + +export type BuilderState = { + flow: Flow; + flowVersion: FlowVersion; + + readonly: boolean; + loopsIndexes: Record; + run: FlowRun | null; + leftSidebar: LeftSideBarType; + rightSidebar: RightSideBarType; + selectedStep: string | null; + canExitRun: boolean; + activeDraggingStep: string | null; + saving: boolean; + refreshBlockFormSettings: boolean; + refreshSettings: () => void; + exitRun: () => void; + exitStepSettings: () => void; + renameFlowClientSide: (newName: string) => void; + moveToFolderClientSide: (folderId: string) => void; + setRun: (run: FlowRun, flowVersion: FlowVersion) => void; + setLeftSidebar: (leftSidebar: LeftSideBarType) => void; + setRightSidebar: (rightSidebar: RightSideBarType) => void; + applyOperation: ( + operation: FlowOperationRequest, + onError: () => void, + ) => void; + removeStepSelection: () => void; + selectStepByName: (stepName: string, openRightSideBar?: boolean) => void; + startSaving: () => void; + setActiveDraggingStep: (stepName: string | null) => void; + setFlow: (flow: Flow) => void; + exitBlockSelector: () => void; + setVersion: (flowVersion: FlowVersion) => void; + setVersionUpdateTimestamp: (updateTimestamp: string) => void; + insertMention: InsertMentionHandler | null; + setReadOnly: (readOnly: boolean) => void; + setInsertMentionHandler: (handler: InsertMentionHandler | null) => void; + setLoopIndex: (stepName: string, index: number) => void; + canUndo: boolean; + setCanUndo: (canUndo: boolean) => void; + canRedo: boolean; + setCanRedo: (canUndo: boolean) => void; + dynamicPropertiesAuthReconnectCounter: number; + refreshDynamicPropertiesForAuth: () => void; + midpanelState: MidpanelState; + applyMidpanelAction: (midpanelAction: MidpanelAction) => void; +}; + +export type BuilderInitialState = Pick< + BuilderState, + 'flow' | 'flowVersion' | 'readonly' | 'run' | 'canExitRun' +>; diff --git a/packages/react-ui/src/app/features/builder/data-selector/index.tsx b/packages/react-ui/src/app/features/builder/data-selector/index.tsx index 7436188382..5aa5fb78e9 100644 --- a/packages/react-ui/src/app/features/builder/data-selector/index.tsx +++ b/packages/react-ui/src/app/features/builder/data-selector/index.tsx @@ -10,8 +10,9 @@ import { useCallback, useState } from 'react'; import { Action, flowHelper, isNil, Trigger } from '@openops/shared'; -import { BuilderState, useBuilderStateContext } from '../builder-hooks'; +import { useBuilderStateContext } from '../builder-hooks'; +import { BuilderState } from '../builder-types'; import { DataSelectorNode } from './data-selector-node'; import { DataSelectorSizeState, diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/flow-builder-canvas.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/flow-builder-canvas.tsx index 0eea361618..b700d9d412 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/flow-builder-canvas.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/flow-builder-canvas.tsx @@ -10,7 +10,8 @@ import { ReturnLoopedgeButton, StepPlaceHolder, } from '@openops/components/ui'; -import { RightSideBarType, useBuilderStateContext } from '../builder-hooks'; +import { useBuilderStateContext } from '../builder-hooks'; +import { RightSideBarType } from '../builder-types'; import { CanvasContextMenuWrapper } from './context-menu/context-menu-wrapper'; import { EdgeWithButton } from './edges/edge-with-button'; import { FlowDragLayer } from './flow-drag-layer'; diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/widgets/incomplete-settings-widget.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/widgets/incomplete-settings-widget.tsx index 18cf1cc4c5..0b3b116b73 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/widgets/incomplete-settings-widget.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/widgets/incomplete-settings-widget.tsx @@ -2,8 +2,8 @@ import { Button } from '@openops/components/ui'; import { t } from 'i18next'; import React, { useMemo } from 'react'; -import { BuilderState } from '@/app/features/builder/builder-hooks'; import { FlowVersion, flowHelper } from '@openops/shared'; +import { BuilderState } from '../../builder-types'; type IncompleteSettingsButtonProps = { flowVersion: FlowVersion; diff --git a/packages/react-ui/src/app/features/builder/flow-version-undo-redo/push-flow-version-to-version-history.ts b/packages/react-ui/src/app/features/builder/flow-version-undo-redo/push-flow-version-to-version-history.ts index 8aa4a0193c..66bf81cd6a 100644 --- a/packages/react-ui/src/app/features/builder/flow-version-undo-redo/push-flow-version-to-version-history.ts +++ b/packages/react-ui/src/app/features/builder/flow-version-undo-redo/push-flow-version-to-version-history.ts @@ -1,4 +1,4 @@ -import { BuilderState } from '@/app/features/builder/builder-hooks'; +import { BuilderState } from '@/app/features/builder/builder-types'; export const pushFlowVersionToVersionHistory = ( currentState: Pick< diff --git a/packages/react-ui/src/app/features/builder/flow-versions/flow-versions-card.tsx b/packages/react-ui/src/app/features/builder/flow-versions/flow-versions-card.tsx index fd52f0b460..af09bc74e6 100644 --- a/packages/react-ui/src/app/features/builder/flow-versions/flow-versions-card.tsx +++ b/packages/react-ui/src/app/features/builder/flow-versions/flow-versions-card.tsx @@ -39,10 +39,8 @@ import { useSearchParams } from 'react-router-dom'; import { useAuthorization } from '@/app/common/hooks/authorization-hooks'; import { SEARCH_PARAMS } from '@/app/constants/search-params'; -import { - LeftSideBarType, - useBuilderStateContext, -} from '@/app/features/builder/builder-hooks'; +import { useBuilderStateContext } from '@/app/features/builder/builder-hooks'; +import { LeftSideBarType } from '@/app/features/builder/builder-types'; import { FlowVersionStateDot } from '@/app/features/flows/components/flow-version-state-dot'; import { flowsApi } from '@/app/features/flows/lib/flows-api'; import { formatUtils } from '@/app/lib/utils'; diff --git a/packages/react-ui/src/app/features/builder/flow-versions/index.tsx b/packages/react-ui/src/app/features/builder/flow-versions/index.tsx index f17d258c12..f919373693 100644 --- a/packages/react-ui/src/app/features/builder/flow-versions/index.tsx +++ b/packages/react-ui/src/app/features/builder/flow-versions/index.tsx @@ -7,12 +7,10 @@ import { import { useQuery } from '@tanstack/react-query'; import { t } from 'i18next'; -import { - LeftSideBarType, - useBuilderStateContext, -} from '@/app/features/builder/builder-hooks'; +import { useBuilderStateContext } from '@/app/features/builder/builder-hooks'; import { flowsApi } from '@/app/features/flows/lib/flows-api'; import { FlowVersionMetadata, SeekPage } from '@openops/shared'; +import { LeftSideBarType } from '../builder-types'; import { FlowVersionDetailsCard } from './flow-versions-card'; diff --git a/packages/react-ui/src/app/features/builder/index.tsx b/packages/react-ui/src/app/features/builder/index.tsx index 8ae1c475e9..0fc5d26c2c 100644 --- a/packages/react-ui/src/app/features/builder/index.tsx +++ b/packages/react-ui/src/app/features/builder/index.tsx @@ -15,8 +15,6 @@ import { useSearchParams } from 'react-router-dom'; import { useMeasure } from 'react-use'; import { - LeftSideBarType, - RightSideBarType, useBuilderStateContext, useSwitchToDraft, } from '@/app/features/builder/builder-hooks'; @@ -50,6 +48,7 @@ import { RunDetailsBar } from '../flow-runs/components/run-details-bar'; import { FlowSideMenu } from '../navigation/side-menu/flow/flow-side-menu'; import LeftSidebarResizablePanel from '../navigation/side-menu/left-sidebar'; import { BuilderHeader } from './builder-header/builder-header'; +import { LeftSideBarType, RightSideBarType } from './builder-types'; import { FlowBuilderCanvas } from './flow-canvas/flow-builder-canvas'; import { FLOW_CANVAS_CONTAINER_ID } from './flow-version-undo-redo/constants'; import { UndoRedo } from './flow-version-undo-redo/undo-redo'; diff --git a/packages/react-ui/src/app/features/builder/run-details/index.tsx b/packages/react-ui/src/app/features/builder/run-details/index.tsx index aa15369002..783029bd93 100644 --- a/packages/react-ui/src/app/features/builder/run-details/index.tsx +++ b/packages/react-ui/src/app/features/builder/run-details/index.tsx @@ -12,10 +12,7 @@ import { ChevronLeft, Info } from 'lucide-react'; import React, { useMemo } from 'react'; import { flagsHooks } from '@/app/common/hooks/flags-hooks'; -import { - LeftSideBarType, - useBuilderStateContext, -} from '@/app/features/builder/builder-hooks'; +import { useBuilderStateContext } from '@/app/features/builder/builder-hooks'; import { FlagId, FlowRun, @@ -23,6 +20,7 @@ import { isNil, RunEnvironment, } from '@openops/shared'; +import { LeftSideBarType } from '../builder-types'; import { flowRunUtils } from '../../flow-runs/lib/flow-run-utils'; import { FlowStepDetailsCardItem } from './flow-step-details-card-item'; diff --git a/packages/react-ui/src/app/features/builder/run-list/flow-run-card.tsx b/packages/react-ui/src/app/features/builder/run-list/flow-run-card.tsx index ca8d741533..1f69d6e20a 100644 --- a/packages/react-ui/src/app/features/builder/run-list/flow-run-card.tsx +++ b/packages/react-ui/src/app/features/builder/run-list/flow-run-card.tsx @@ -12,15 +12,13 @@ import { t } from 'i18next'; import { ChevronRightIcon } from 'lucide-react'; import React from 'react'; -import { - LeftSideBarType, - useBuilderStateContext, -} from '@/app/features/builder/builder-hooks'; +import { useBuilderStateContext } from '@/app/features/builder/builder-hooks'; import { flowRunUtils } from '@/app/features/flow-runs/lib/flow-run-utils'; import { flowRunsApi } from '@/app/features/flow-runs/lib/flow-runs-api'; import { flowsApi } from '@/app/features/flows/lib/flows-api'; import { formatUtils } from '@/app/lib/utils'; import { FlowRun, isNil, PopulatedFlow } from '@openops/shared'; +import { LeftSideBarType } from '../builder-types'; type FlowRunCardProps = { run: FlowRun; diff --git a/packages/react-ui/src/app/features/builder/run-list/index.tsx b/packages/react-ui/src/app/features/builder/run-list/index.tsx index 112e209ed7..f18dc41bc7 100644 --- a/packages/react-ui/src/app/features/builder/run-list/index.tsx +++ b/packages/react-ui/src/app/features/builder/run-list/index.tsx @@ -10,12 +10,10 @@ import { useQuery } from '@tanstack/react-query'; import { t } from 'i18next'; import React from 'react'; -import { - LeftSideBarType, - useBuilderStateContext, -} from '@/app/features/builder/builder-hooks'; +import { useBuilderStateContext } from '@/app/features/builder/builder-hooks'; import { flowRunsApi } from '@/app/features/flow-runs/lib/flow-runs-api'; import { FlowRun, SeekPage } from '@openops/shared'; +import { LeftSideBarType } from '../builder-types'; import { FlowRunCard } from './flow-run-card'; diff --git a/packages/react-ui/src/app/features/builder/tests/update-flow-version.test.ts b/packages/react-ui/src/app/features/builder/tests/update-flow-version.test.ts new file mode 100644 index 0000000000..977fef7a06 --- /dev/null +++ b/packages/react-ui/src/app/features/builder/tests/update-flow-version.test.ts @@ -0,0 +1,126 @@ +import { ActionType, FlowOperationType, TriggerType } from '@openops/shared'; +import { waitFor } from '@testing-library/react'; +import { aiChatApi } from '../ai-chat/lib/chat-api'; +import { BuilderState, RightSideBarType } from '../builder-types'; +import { updateFlowVersion } from '../update-flow-version'; + +jest.mock('@/app/features/flows/lib/flows-api'); +jest.mock('../ai-chat/lib/chat-api'); + +describe('updateFlowVersion', () => { + let mockState: BuilderState; + let mockSet: () => void; + let mockOnError: () => void; + + beforeEach(() => { + mockState = { + flow: { id: 'flow1' }, + flowVersion: { + id: 'version1', + trigger: { + name: 'trigger1', + type: TriggerType.EMPTY, + settings: {}, + valid: false, + displayName: 'Select Trigger', + nextAction: { + name: 'step_1', + type: ActionType.BLOCK, + valid: false, + displayName: 'step1', + settings: { + blockVersion: '1.0.0', + }, + }, + }, + }, + selectedStep: 'step_1', + rightSidebar: RightSideBarType.BLOCK_SETTINGS, + }; + + mockSet = jest.fn(); + mockOnError = jest.fn(); + }); + + it('should update flow version when operation is applied', async () => { + const operation = { + type: FlowOperationType.UPDATE_ACTION, + request: { + name: 'step_1', + type: 'BLOCK', + valid: false, + settings: { blockVersion: '1.0.0' }, + displayName: 'Google Cloud CLI', + }, + }; + + const result = updateFlowVersion( + mockState, + operation, + mockOnError, + mockSet, + ); + + await waitFor(() => expect(mockSet).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(mockSet).toHaveBeenCalledWith({ saving: true })); + + expect(result).toEqual({ + flowVersion: { + id: 'version1', + trigger: { + name: 'trigger1', + type: 'EMPTY', + settings: {}, + valid: false, + displayName: 'Select Trigger', + nextAction: { + displayName: 'Google Cloud CLI', + name: 'step_1', + valid: false, + type: 'BLOCK', + settings: { blockVersion: '^1.0.0' }, + }, + }, + valid: false, + }, + }); + }); + + it('should handle delete action and clear selection when deleting selected step', async () => { + const operation = { + type: FlowOperationType.DELETE_ACTION, + request: { name: 'step_1' }, + }; + + (aiChatApi.open as jest.Mock).mockResolvedValue({ chatId: 'chat1' }); + (aiChatApi.delete as jest.Mock).mockResolvedValue({}); + + updateFlowVersion(mockState, operation, mockOnError, mockSet); + + expect(mockSet).toHaveBeenCalledWith({ selectedStep: undefined }); + expect(mockSet).toHaveBeenCalledWith({ + rightSidebar: RightSideBarType.NONE, + }); + + await waitFor(() => { + expect(aiChatApi.open).toHaveBeenCalled(); + }); + await waitFor(() => { + expect(aiChatApi.delete).toHaveBeenCalled(); + }); + }); + + it('should handle duplicate action selecting new step', async () => { + const operation = { + type: FlowOperationType.DUPLICATE_ACTION, + request: { stepName: 'step_1' }, + }; + + updateFlowVersion(mockState, operation, mockOnError, mockSet); + + await waitFor(() => expect(mockSet).toHaveBeenCalledWith({ saving: true })); + await waitFor(() => + expect(mockSet).toHaveBeenCalledWith({ selectedStep: 'step_2' }), + ); + }); +}); diff --git a/packages/react-ui/src/app/features/builder/tree-view/tree-view-sidebar.tsx b/packages/react-ui/src/app/features/builder/tree-view/tree-view-sidebar.tsx index c6faebf8f3..f72a9e217a 100644 --- a/packages/react-ui/src/app/features/builder/tree-view/tree-view-sidebar.tsx +++ b/packages/react-ui/src/app/features/builder/tree-view/tree-view-sidebar.tsx @@ -3,7 +3,8 @@ import { useCallback, useMemo } from 'react'; import { flowHelper } from '@openops/shared'; -import { LeftSideBarType, useBuilderStateContext } from '../builder-hooks'; +import { useBuilderStateContext } from '../builder-hooks'; +import { LeftSideBarType } from '../builder-types'; import { useCenterWorkflowViewOntoStep } from '../hooks/center-workflow-view-onto-step'; import { mapStepsToTreeView } from './utils'; diff --git a/packages/react-ui/src/app/features/builder/update-flow-version.ts b/packages/react-ui/src/app/features/builder/update-flow-version.ts new file mode 100644 index 0000000000..24adc6b789 --- /dev/null +++ b/packages/react-ui/src/app/features/builder/update-flow-version.ts @@ -0,0 +1,82 @@ +import { PromiseQueue } from '@/app/lib/promise-queue'; +import { + flowHelper, + FlowOperationRequest, + FlowOperationType, + FlowVersion, +} from '@openops/shared'; +import { flowsApi } from '../flows/lib/flows-api'; +import { aiChatApi } from './ai-chat/lib/chat-api'; +import { BuilderState, RightSideBarType } from './builder-types'; + +const flowUpdatesQueue = new PromiseQueue(); + +export const updateFlowVersion = ( + state: BuilderState, + operation: FlowOperationRequest, + onError: () => void, + set: ( + partial: + | BuilderState + | Partial + | ((state: BuilderState) => BuilderState | Partial), + replace?: boolean | undefined, + ) => void, +) => { + const newFlowVersion = flowHelper.apply(state.flowVersion, operation); + if ( + operation.type === FlowOperationType.DELETE_ACTION && + operation.request.name === state.selectedStep + ) { + set({ selectedStep: undefined }); + set({ rightSidebar: RightSideBarType.NONE }); + deleteChatRequest(state.flowVersion, operation.request.name); + } + + if (operation.type === FlowOperationType.DUPLICATE_ACTION) { + set({ + selectedStep: flowHelper.getStep( + newFlowVersion, + operation.request.stepName, + )?.nextAction?.name, + }); + } + + const updateRequest = async () => { + set({ saving: true }); + try { + const updatedFlowVersion = await flowsApi.update( + state.flow.id, + operation, + ); + set((state) => { + return { + flowVersion: { + ...state.flowVersion, + id: updatedFlowVersion.version.id, + state: updatedFlowVersion.version.state, + updated: updatedFlowVersion.version.updated, + }, + saving: flowUpdatesQueue.size() !== 0, + }; + }); + } catch (error) { + console.error(error); + flowUpdatesQueue.halt(); + onError(); + } + }; + flowUpdatesQueue.add(updateRequest); + return { flowVersion: newFlowVersion }; +}; + +async function deleteChatRequest(flowVersion: FlowVersion, stepName: string) { + try { + const stepDetails = flowHelper.getStep(flowVersion, stepName); + const blockName = stepDetails?.settings?.blockName; + const chat = await aiChatApi.open(flowVersion.flowId, blockName, stepName); + await aiChatApi.delete(chat.chatId); + } catch (err) { + console.error(err); + } +} diff --git a/packages/react-ui/src/app/features/navigation/side-menu/flow/flow-side-menu-header.tsx b/packages/react-ui/src/app/features/navigation/side-menu/flow/flow-side-menu-header.tsx index 6894a7c53f..872fbec970 100644 --- a/packages/react-ui/src/app/features/navigation/side-menu/flow/flow-side-menu-header.tsx +++ b/packages/react-ui/src/app/features/navigation/side-menu/flow/flow-side-menu-header.tsx @@ -2,10 +2,8 @@ import { Button, SideMenuHeader, TooltipWrapper } from '@openops/components/ui'; import { PanelRight } from 'lucide-react'; import { AppLogo } from '@/app/common/components/app-logo'; -import { - LeftSideBarType, - useBuilderStateContext, -} from '@/app/features/builder/builder-hooks'; +import { useBuilderStateContext } from '@/app/features/builder/builder-hooks'; +import { LeftSideBarType } from '@/app/features/builder/builder-types'; import { useAppStore } from '@/app/store/app-store'; import { t } from 'i18next'; import { useCallback } from 'react';