diff --git a/packages/backend/src/graphql/queries/get-flow.js b/packages/backend/src/graphql/queries/get-flow.js deleted file mode 100644 index 3c1b750b..00000000 --- a/packages/backend/src/graphql/queries/get-flow.js +++ /dev/null @@ -1,19 +0,0 @@ -import Flow from '../../models/flow.js'; - -const getFlow = async (_parent, params, context) => { - const conditions = context.currentUser.can('read', 'Flow'); - const userFlows = context.currentUser.$relatedQuery('flows'); - const allFlows = Flow.query(); - const baseQuery = conditions.isCreator ? userFlows : allFlows; - - const flow = await baseQuery - .clone() - .withGraphJoined('[steps.[connection]]') - .orderBy('steps.position', 'asc') - .findOne({ 'flows.id': params.id }) - .throwIfNotFound(); - - return flow; -}; - -export default getFlow; diff --git a/packages/backend/src/graphql/queries/get-flow.test.js b/packages/backend/src/graphql/queries/get-flow.test.js deleted file mode 100644 index 5a5f106f..00000000 --- a/packages/backend/src/graphql/queries/get-flow.test.js +++ /dev/null @@ -1,240 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import request from 'supertest'; -import app from '../../app'; -import appConfig from '../../config/app'; -import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id'; -import { createRole } from '../../../test/factories/role'; -import { createPermission } from '../../../test/factories/permission'; -import { createUser } from '../../../test/factories/user'; -import { createFlow } from '../../../test/factories/flow'; -import { createStep } from '../../../test/factories/step'; -import { createConnection } from '../../../test/factories/connection'; - -describe('graphQL getFlow query', () => { - const query = (flowId) => { - return ` - query { - getFlow(id: "${flowId}") { - id - name - active - status - steps { - id - type - key - appKey - iconUrl - webhookUrl - status - position - connection { - id - verified - createdAt - } - parameters - } - } - } - `; - }; - - describe('and without permissions', () => { - it('should throw not authorized error', async () => { - const userWithoutPermissions = await createUser(); - const token = createAuthTokenByUserId(userWithoutPermissions.id); - const flow = await createFlow(); - - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query: query(flow.id) }) - .expect(200); - - expect(response.body.errors).toBeDefined(); - expect(response.body.errors[0].message).toEqual('Not authorized!'); - }); - }); - - describe('and with correct permission', () => { - let currentUser, currentUserRole, currentUserFlow; - - beforeEach(async () => { - currentUserRole = await createRole(); - currentUser = await createUser({ roleId: currentUserRole.id }); - currentUserFlow = await createFlow({ userId: currentUser.id }); - }); - - describe('and with isCreator condition', () => { - it('should return executions data of the current user', async () => { - await createPermission({ - action: 'read', - subject: 'Flow', - roleId: currentUserRole.id, - conditions: ['isCreator'], - }); - - const triggerStep = await createStep({ - flowId: currentUserFlow.id, - type: 'trigger', - key: 'catchRawWebhook', - webhookPath: `/webhooks/flows/${currentUserFlow.id}`, - }); - - const actionConnection = await createConnection({ - userId: currentUser.id, - formattedData: { - screenName: 'Test', - authenticationKey: 'test key', - }, - }); - - const actionStep = await createStep({ - flowId: currentUserFlow.id, - type: 'action', - connectionId: actionConnection.id, - key: 'translateText', - }); - - const token = createAuthTokenByUserId(currentUser.id); - - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query: query(currentUserFlow.id) }) - .expect(200); - - const expectedResponsePayload = { - data: { - getFlow: { - active: currentUserFlow.active, - id: currentUserFlow.id, - name: currentUserFlow.name, - status: 'draft', - steps: [ - { - appKey: triggerStep.appKey, - connection: null, - iconUrl: `${appConfig.baseUrl}/apps/${triggerStep.appKey}/assets/favicon.svg`, - id: triggerStep.id, - key: 'catchRawWebhook', - parameters: {}, - position: 1, - status: triggerStep.status, - type: 'trigger', - webhookUrl: `${appConfig.baseUrl}/webhooks/flows/${currentUserFlow.id}`, - }, - { - appKey: actionStep.appKey, - connection: { - createdAt: actionConnection.createdAt.getTime().toString(), - id: actionConnection.id, - verified: actionConnection.verified, - }, - iconUrl: `${appConfig.baseUrl}/apps/${actionStep.appKey}/assets/favicon.svg`, - id: actionStep.id, - key: 'translateText', - parameters: {}, - position: 2, - status: actionStep.status, - type: 'action', - webhookUrl: 'http://localhost:3000/null', - }, - ], - }, - }, - }; - - expect(response.body).toEqual(expectedResponsePayload); - }); - }); - - describe('and without isCreator condition', () => { - it('should return executions data of all users', async () => { - await createPermission({ - action: 'read', - subject: 'Flow', - roleId: currentUserRole.id, - conditions: [], - }); - - const anotherUser = await createUser(); - const anotherUserFlow = await createFlow({ userId: anotherUser.id }); - - const triggerStep = await createStep({ - flowId: anotherUserFlow.id, - type: 'trigger', - key: 'catchRawWebhook', - webhookPath: `/webhooks/flows/${anotherUserFlow.id}`, - }); - - const actionConnection = await createConnection({ - userId: anotherUser.id, - formattedData: { - screenName: 'Test', - authenticationKey: 'test key', - }, - }); - - const actionStep = await createStep({ - flowId: anotherUserFlow.id, - type: 'action', - connectionId: actionConnection.id, - key: 'translateText', - }); - - const token = createAuthTokenByUserId(currentUser.id); - - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query: query(anotherUserFlow.id) }) - .expect(200); - - const expectedResponsePayload = { - data: { - getFlow: { - active: anotherUserFlow.active, - id: anotherUserFlow.id, - name: anotherUserFlow.name, - status: 'draft', - steps: [ - { - appKey: triggerStep.appKey, - connection: null, - iconUrl: `${appConfig.baseUrl}/apps/${triggerStep.appKey}/assets/favicon.svg`, - id: triggerStep.id, - key: 'catchRawWebhook', - parameters: {}, - position: 1, - status: triggerStep.status, - type: 'trigger', - webhookUrl: `${appConfig.baseUrl}/webhooks/flows/${anotherUserFlow.id}`, - }, - { - appKey: actionStep.appKey, - connection: { - createdAt: actionConnection.createdAt.getTime().toString(), - id: actionConnection.id, - verified: actionConnection.verified, - }, - iconUrl: `${appConfig.baseUrl}/apps/${actionStep.appKey}/assets/favicon.svg`, - id: actionStep.id, - key: 'translateText', - parameters: {}, - position: 2, - status: actionStep.status, - type: 'action', - webhookUrl: 'http://localhost:3000/null', - }, - ], - }, - }, - }; - - expect(response.body).toEqual(expectedResponsePayload); - }); - }); - }); -}); diff --git a/packages/backend/src/graphql/query-resolvers.js b/packages/backend/src/graphql/query-resolvers.js index d7941d9c..30e2c3ed 100644 --- a/packages/backend/src/graphql/query-resolvers.js +++ b/packages/backend/src/graphql/query-resolvers.js @@ -4,7 +4,6 @@ import getAppAuthClients from './queries/get-app-auth-clients.ee.js'; import getBillingAndUsage from './queries/get-billing-and-usage.ee.js'; import getConnectedApps from './queries/get-connected-apps.js'; import getDynamicData from './queries/get-dynamic-data.js'; -import getFlow from './queries/get-flow.js'; import getStepWithTestExecutions from './queries/get-step-with-test-executions.js'; import testConnection from './queries/test-connection.js'; @@ -15,7 +14,6 @@ const queryResolvers = { getBillingAndUsage, getConnectedApps, getDynamicData, - getFlow, getStepWithTestExecutions, testConnection, }; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 356df968..bd27be8f 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -4,7 +4,6 @@ type Query { getAppAuthClients(appKey: String!, active: Boolean): [AppAuthClient] getConnectedApps(name: String): [App] testConnection(id: String!): Connection - getFlow(id: String!): Flow getStepWithTestExecutions(stepId: String!): [Step] getDynamicData( stepId: String! diff --git a/packages/web/src/components/ChooseConnectionSubstep/index.jsx b/packages/web/src/components/ChooseConnectionSubstep/index.jsx index 33ea0a17..88fe0508 100644 --- a/packages/web/src/components/ChooseConnectionSubstep/index.jsx +++ b/packages/web/src/components/ChooseConnectionSubstep/index.jsx @@ -21,6 +21,8 @@ import { StepPropType, SubstepPropType, } from 'propTypes/propTypes'; +import useStepConnection from 'hooks/useStepConnection'; +import { useQueryClient } from '@tanstack/react-query'; const ADD_CONNECTION_VALUE = 'ADD_CONNECTION'; const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION'; @@ -44,13 +46,14 @@ function ChooseConnectionSubstep(props) { onChange, application, } = props; - const { connection, appKey } = step; + const { appKey } = step; const formatMessage = useFormatMessage(); const editorContext = React.useContext(EditorContext); const [showAddConnectionDialog, setShowAddConnectionDialog] = React.useState(false); const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] = React.useState(false); + const queryClient = useQueryClient(); const { authenticate } = useAuthenticateApp({ appKey: application.key, @@ -63,21 +66,24 @@ function ChooseConnectionSubstep(props) { const { data: appConfig } = useAppConfig(application.key); + const { data: stepConnectionData } = useStepConnection(step.id); + const stepConnection = stepConnectionData?.data; + // TODO: show detailed error when connection test/verification fails const [ testConnection, { loading: testResultLoading, refetch: retestConnection }, ] = useLazyQuery(TEST_CONNECTION, { variables: { - id: connection?.id, + id: stepConnection?.id, }, }); React.useEffect(() => { - if (connection?.id) { + if (stepConnection?.id) { testConnection({ variables: { - id: connection.id, + id: stepConnection.id, }, }); } @@ -154,8 +160,9 @@ function ChooseConnectionSubstep(props) { }, [onChange, refetch, step], ); + const handleChange = React.useCallback( - (event, selectedOption) => { + async (event, selectedOption) => { if (typeof selectedOption === 'object') { // TODO: try to simplify type casting below. const typedSelectedOption = selectedOption; @@ -172,7 +179,7 @@ function ChooseConnectionSubstep(props) { return; } - if (connectionId !== step.connection?.id) { + if (connectionId !== stepConnection?.id) { onChange({ step: { ...step, @@ -181,19 +188,23 @@ function ChooseConnectionSubstep(props) { }, }, }); + + await queryClient.invalidateQueries({ + queryKey: ['stepConnection', step.id], + }); } } }, - [step, onChange], + [step, onChange, queryClient], ); React.useEffect(() => { - if (step.connection?.id) { + if (stepConnection?.id) { retestConnection({ - id: step.connection.id, + id: stepConnection?.id, }); } - }, [step.connection?.id, retestConnection]); + }, [stepConnection?.id, retestConnection]); const onToggle = expanded ? onCollapse : onExpand; @@ -203,7 +214,7 @@ function ChooseConnectionSubstep(props) { expanded={expanded} onClick={onToggle} title={name} - valid={testResultLoading ? null : connection?.verified} + valid={testResultLoading ? null : stepConnection?.verified} /> )} - value={getOption(connectionOptions, connection?.id)} + value={getOption(connectionOptions, stepConnection?.id)} onChange={handleChange} loading={loading} data-test="choose-connection-autocomplete" @@ -242,7 +253,7 @@ function ChooseConnectionSubstep(props) { sx={{ mt: 2 }} disabled={ testResultLoading || - !connection?.verified || + !stepConnection?.verified || editorContext.readOnly } data-test="flow-substep-continue-button" diff --git a/packages/web/src/components/Editor/index.jsx b/packages/web/src/components/Editor/index.jsx index 4c133a92..0458094d 100644 --- a/packages/web/src/components/Editor/index.jsx +++ b/packages/web/src/components/Editor/index.jsx @@ -3,47 +3,24 @@ import { useMutation } from '@apollo/client'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import AddIcon from '@mui/icons-material/Add'; -import { GET_FLOW } from 'graphql/queries/get-flow'; + import { CREATE_STEP } from 'graphql/mutations/create-step'; import { UPDATE_STEP } from 'graphql/mutations/update-step'; import FlowStep from 'components/FlowStep'; import { FlowPropType } from 'propTypes/propTypes'; - -function updateHandlerFactory(flowId, previousStepId) { - return function createStepUpdateHandler(cache, mutationResult) { - const { data } = mutationResult; - const { createStep: createdStep } = data; - const { getFlow: flow } = cache.readQuery({ - query: GET_FLOW, - variables: { id: flowId }, - }); - const steps = flow.steps.reduce((steps, currentStep) => { - if (currentStep.id === previousStepId) { - return [...steps, currentStep, createdStep]; - } - return [...steps, currentStep]; - }, []); - cache.writeQuery({ - query: GET_FLOW, - variables: { id: flowId }, - data: { getFlow: { ...flow, steps } }, - }); - }; -} +import { useQueryClient } from '@tanstack/react-query'; function Editor(props) { const [updateStep] = useMutation(UPDATE_STEP); - const [createStep, { loading: creationInProgress }] = useMutation( - CREATE_STEP, - { - refetchQueries: ['GetFlow'], - }, - ); + const [createStep, { loading: creationInProgress }] = + useMutation(CREATE_STEP); const { flow } = props; const [triggerStep] = flow.steps; const [currentStepId, setCurrentStepId] = React.useState(triggerStep.id); + const queryClient = useQueryClient(); + const onStepChange = React.useCallback( - (step) => { + async (step) => { const mutationInput = { id: step.id, key: step.key, @@ -55,13 +32,20 @@ function Editor(props) { id: flow.id, }, }; + if (step.appKey) { mutationInput.appKey = step.appKey; } - updateStep({ variables: { input: mutationInput } }); + + await updateStep({ variables: { input: mutationInput } }); + await queryClient.invalidateQueries({ + queryKey: ['stepConnection', step.id], + }); + await queryClient.invalidateQueries({ queryKey: ['flow', flow.id] }); }, - [updateStep, flow.id], + [updateStep, flow.id, queryClient], ); + const addStep = React.useCallback( async (previousStepId) => { const mutationInput = { @@ -72,20 +56,24 @@ function Editor(props) { id: flow.id, }, }; + const createdStep = await createStep({ variables: { input: mutationInput }, - update: updateHandlerFactory(flow.id, previousStepId), }); + const createdStepId = createdStep.data.createStep.id; setCurrentStepId(createdStepId); + await queryClient.invalidateQueries({ queryKey: ['flow', flow.id] }); }, - [createStep, flow.id], + [createStep, flow.id, queryClient], ); + const openNextStep = React.useCallback((nextStep) => { return () => { setCurrentStepId(nextStep?.id); }; }, []); + return ( setCurrentStepId(step.id)} onClose={() => setCurrentStepId(null)} onChange={onStepChange} + flowId={flow.id} onContinue={openNextStep(steps[index + 1])} /> diff --git a/packages/web/src/components/EditorLayout/index.jsx b/packages/web/src/components/EditorLayout/index.jsx index 2c6ceaa9..f092ff69 100644 --- a/packages/web/src/components/EditorLayout/index.jsx +++ b/packages/web/src/components/EditorLayout/index.jsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Link, useParams } from 'react-router-dom'; -import { useMutation, useQuery } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import Stack from '@mui/material/Stack'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -8,6 +8,7 @@ import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; import Snackbar from '@mui/material/Snackbar'; + import { EditorProvider } from 'contexts/Editor'; import EditableTypography from 'components/EditableTypography'; import Container from 'components/Container'; @@ -15,17 +16,20 @@ import Editor from 'components/Editor'; import useFormatMessage from 'hooks/useFormatMessage'; import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status'; import { UPDATE_FLOW } from 'graphql/mutations/update-flow'; -import { GET_FLOW } from 'graphql/queries/get-flow'; import * as URLS from 'config/urls'; import { TopBar } from './style'; +import useFlow from 'hooks/useFlow'; +import { useQueryClient } from '@tanstack/react-query'; export default function EditorLayout() { const { flowId } = useParams(); const formatMessage = useFormatMessage(); const [updateFlow] = useMutation(UPDATE_FLOW); const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS); - const { data, loading } = useQuery(GET_FLOW, { variables: { id: flowId } }); - const flow = data?.getFlow; + const { data, isLoading: isFlowLoading } = useFlow(flowId); + const flow = data?.data; + const queryClient = useQueryClient(); + const onFlowNameUpdate = React.useCallback( async (name) => { await updateFlow({ @@ -38,14 +42,17 @@ export default function EditorLayout() { optimisticResponse: { updateFlow: { __typename: 'Flow', - id: flow?.id, + id: flowId, name, }, }, }); + + await queryClient.invalidateQueries({ queryKey: ['flow', flowId] }); }, - [flow?.id], + [flowId, queryClient], ); + const onFlowStatusUpdate = React.useCallback( async (active) => { await updateFlowStatus({ @@ -58,14 +65,17 @@ export default function EditorLayout() { optimisticResponse: { updateFlowStatus: { __typename: 'Flow', - id: flow?.id, + id: flowId, active, }, }, }); + + await queryClient.invalidateQueries({ queryKey: ['flow', flowId] }); }, - [flow?.id], + [flowId, queryClient], ); + return ( <> - {!loading && ( + {!isFlowLoading && ( - {!flow && !loading && 'not found'} + {!flow && !isFlowLoading && 'not found'} {flow && } diff --git a/packages/web/src/components/FlowContextMenu/index.jsx b/packages/web/src/components/FlowContextMenu/index.jsx index 713ac779..1d124ceb 100644 --- a/packages/web/src/components/FlowContextMenu/index.jsx +++ b/packages/web/src/components/FlowContextMenu/index.jsx @@ -5,6 +5,7 @@ import MenuItem from '@mui/material/MenuItem'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import { Link } from 'react-router-dom'; + import Can from 'components/Can'; import * as URLS from 'config/urls'; import { DELETE_FLOW } from 'graphql/mutations/delete-flow'; @@ -12,25 +13,28 @@ import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow'; import useFormatMessage from 'hooks/useFormatMessage'; function ContextMenu(props) { - const { flowId, onClose, anchorEl } = props; + const { flowId, onClose, anchorEl, onDuplicateFlow, onDeleteFlow } = props; const enqueueSnackbar = useEnqueueSnackbar(); const [deleteFlow] = useMutation(DELETE_FLOW); - const [duplicateFlow] = useMutation(DUPLICATE_FLOW, { - refetchQueries: ['GetFlows'], - }); + const [duplicateFlow] = useMutation(DUPLICATE_FLOW); const formatMessage = useFormatMessage(); + const onFlowDuplicate = React.useCallback(async () => { await duplicateFlow({ variables: { input: { id: flowId } }, }); + enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), { variant: 'success', SnackbarProps: { 'data-test': 'snackbar-duplicate-flow-success', }, }); + + onDuplicateFlow?.(); onClose(); - }, [flowId, onClose, duplicateFlow]); + }, [flowId, onClose, duplicateFlow, onDuplicateFlow]); + const onFlowDelete = React.useCallback(async () => { await deleteFlow({ variables: { input: { id: flowId } }, @@ -44,11 +48,15 @@ function ContextMenu(props) { }); }, }); + enqueueSnackbar(formatMessage('flow.successfullyDeleted'), { variant: 'success', }); + + onDeleteFlow?.(); onClose(); - }, [flowId, onClose, deleteFlow]); + }, [flowId, onClose, deleteFlow, onDeleteFlow]); + return ( { setAnchorEl(null); }; @@ -112,6 +114,8 @@ function FlowRow(props) { flowId={flow.id} onClose={handleClose} anchorEl={anchorEl} + onDeleteFlow={onDeleteFlow} + onDuplicateFlow={onDuplicateFlow} /> )} @@ -120,6 +124,8 @@ function FlowRow(props) { FlowRow.propTypes = { flow: FlowPropType.isRequired, + onDeleteFlow: PropTypes.func, + onDuplicateFlow: PropTypes.func, }; export default FlowRow; diff --git a/packages/web/src/components/FlowStep/index.jsx b/packages/web/src/components/FlowStep/index.jsx index b7c8f222..b5bb79b9 100644 --- a/packages/web/src/components/FlowStep/index.jsx +++ b/packages/web/src/components/FlowStep/index.jsx @@ -105,7 +105,7 @@ function generateValidationSchema(substeps) { } function FlowStep(props) { - const { collapsed, onChange, onContinue } = props; + const { collapsed, onChange, onContinue, flowId } = props; const editorContext = React.useContext(EditorContext); const contextButtonRef = React.useRef(null); const step = props.step; @@ -328,6 +328,7 @@ function FlowStep(props) { : false } step={step} + flowId={flowId} /> )} @@ -363,6 +364,7 @@ function FlowStep(props) { deletable={!isTrigger} onClose={onContextMenuClose} anchorEl={anchorEl} + flowId={flowId} /> )} diff --git a/packages/web/src/components/FlowStepContextMenu/index.jsx b/packages/web/src/components/FlowStepContextMenu/index.jsx index e6e9e05a..131c3db7 100644 --- a/packages/web/src/components/FlowStepContextMenu/index.jsx +++ b/packages/web/src/components/FlowStepContextMenu/index.jsx @@ -1,24 +1,31 @@ import PropTypes from 'prop-types'; import * as React from 'react'; import { useMutation } from '@apollo/client'; + import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import { DELETE_STEP } from 'graphql/mutations/delete-step'; import useFormatMessage from 'hooks/useFormatMessage'; +import { useQueryClient } from '@tanstack/react-query'; function FlowStepContextMenu(props) { - const { stepId, onClose, anchorEl, deletable } = props; - const [deleteStep] = useMutation(DELETE_STEP, { - refetchQueries: ['GetFlow', 'GetStepWithTestExecutions'], - }); + const { stepId, onClose, anchorEl, deletable, flowId } = props; const formatMessage = useFormatMessage(); + const queryClient = useQueryClient(); + + const [deleteStep] = useMutation(DELETE_STEP, { + refetchQueries: ['GetStepWithTestExecutions'], + }); + const deleteActionHandler = React.useCallback( async (event) => { event.stopPropagation(); await deleteStep({ variables: { input: { id: stepId } } }); + await queryClient.invalidateQueries({ queryKey: ['flow', flowId] }); }, - [stepId], + [stepId, queryClient], ); + return ( { try { @@ -28,6 +31,7 @@ function serializeErrors(graphQLErrors) { } }); } + function TestSubstep(props) { const { substep, @@ -38,6 +42,7 @@ function TestSubstep(props) { onContinue, step, showWebhookUrl = false, + flowId, } = props; const formatMessage = useFormatMessage(); const editorContext = React.useContext(EditorContext); @@ -52,6 +57,8 @@ function TestSubstep(props) { const isCompleted = !error && called && !loading; const hasNoOutput = !response && isCompleted; const { name } = substep; + const queryClient = useQueryClient(); + React.useEffect( function resetTestDataOnSubstepToggle() { if (!expanded) { @@ -60,20 +67,28 @@ function TestSubstep(props) { }, [expanded, reset], ); - const handleSubmit = React.useCallback(() => { + + const handleSubmit = React.useCallback(async () => { if (isCompleted) { onContinue?.(); return; } - executeFlow({ + + await executeFlow({ variables: { input: { stepId: step.id, }, }, }); - }, [onSubmit, onContinue, isCompleted, step.id]); + + await queryClient.invalidateQueries({ + queryKey: ['flow', flowId], + }); + }, [onSubmit, onContinue, isCompleted, step.id, queryClient, flowId]); + const onToggle = expanded ? onCollapse : onExpand; + return ( diff --git a/packages/web/src/graphql/queries/get-flow.js b/packages/web/src/graphql/queries/get-flow.js deleted file mode 100644 index 8ef0bf67..00000000 --- a/packages/web/src/graphql/queries/get-flow.js +++ /dev/null @@ -1,27 +0,0 @@ -import { gql } from '@apollo/client'; -export const GET_FLOW = gql` - query GetFlow($id: String!) { - getFlow(id: $id) { - id - name - active - status - steps { - id - type - key - appKey - iconUrl - webhookUrl - status - position - connection { - id - verified - createdAt - } - parameters - } - } - } -`; diff --git a/packages/web/src/hooks/useFlow.js b/packages/web/src/hooks/useFlow.js new file mode 100644 index 00000000..8502d6b1 --- /dev/null +++ b/packages/web/src/hooks/useFlow.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useFlow(flowId) { + const query = useQuery({ + queryKey: ['flow', flowId], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/flows/${flowId}`, { + signal, + }); + + return data; + }, + enabled: !!flowId, + }); + + return query; +} diff --git a/packages/web/src/hooks/useStepConnection.js b/packages/web/src/hooks/useStepConnection.js new file mode 100644 index 00000000..dace7518 --- /dev/null +++ b/packages/web/src/hooks/useStepConnection.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useStepConnection(stepId) { + const query = useQuery({ + queryKey: ['stepConnection', stepId], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/steps/${stepId}/connection`, { + signal, + }); + + return data; + }, + enabled: !!stepId, + }); + + return query; +} diff --git a/packages/web/src/pages/Flows/index.jsx b/packages/web/src/pages/Flows/index.jsx index f59175f2..22544394 100644 --- a/packages/web/src/pages/Flows/index.jsx +++ b/packages/web/src/pages/Flows/index.jsx @@ -27,7 +27,7 @@ export default function Flows() { const [flowName, setFlowName] = React.useState(''); const [isLoading, setIsLoading] = React.useState(false); - const { data, mutate } = useLazyFlows( + const { data, mutate: fetchFlows } = useLazyFlows( { flowName, page }, { onSettled: () => { @@ -36,7 +36,10 @@ export default function Flows() { }, ); - const fetchData = React.useMemo(() => debounce(mutate, 300), [mutate]); + const fetchData = React.useMemo( + () => debounce(fetchFlows, 300), + [fetchFlows], + ); React.useEffect(() => { setIsLoading(true); @@ -110,7 +113,14 @@ export default function Flows() { )} {!isLoading && - flows?.map((flow) => )} + flows?.map((flow) => ( + + ))} {!isLoading && !hasFlows && (