From c5c43def755363c7cd2d9e34b0374130626aae30 Mon Sep 17 00:00:00 2001 From: Oscar Reyes Date: Thu, 23 Nov 2023 16:16:54 -0600 Subject: [PATCH] feat: Duplicate (#3396) * feat: Duplicate * improving UX * improving UX * PR improvements --- .../ConfirmationModal/ConfirmationModal.tsx | 3 ++ .../ResourceCard/ResourceCardActions.tsx | 37 ++++++++++--------- web/src/components/ResourceCard/TestCard.tsx | 4 +- .../components/ResourceCard/TestSuiteCard.tsx | 3 ++ .../__tests__/ResourceCardActions.test.tsx | 10 ++++- .../ResourceCard/__tests__/TestCard.test.tsx | 9 ++++- web/src/components/TestHeader/TestHeader.tsx | 11 +++++- .../TestHeader/__tests__/TestHeader.test.tsx | 11 +++++- web/src/hooks/useKeyEvent.ts | 34 +++++++++++++++++ web/src/models/TestOutput.model.ts | 4 +- web/src/pages/Home/TestsList.tsx | 17 ++++++++- web/src/pages/Test/Content.tsx | 16 +++++++- web/src/pages/TestSuite/Content.tsx | 14 ++++++- web/src/pages/TestSuites/TestSuitesList.tsx | 17 ++++++++- .../CreateTest/CreateTest.provider.tsx | 11 ++---- .../CreateTestSuite.provider.tsx | 11 ++---- web/src/providers/Test/hooks/useTestCrud.ts | 25 ++++++++++++- .../TestSuite/hooks/useTestSuiteCrud.ts | 23 ++++++++++++ web/src/services/Test.service.ts | 5 +++ web/src/services/TestSuite.service.ts | 6 +++ web/src/types/TestSuite.types.ts | 2 + 21 files changed, 224 insertions(+), 49 deletions(-) create mode 100644 web/src/hooks/useKeyEvent.ts diff --git a/web/src/components/ConfirmationModal/ConfirmationModal.tsx b/web/src/components/ConfirmationModal/ConfirmationModal.tsx index b231d79163..c935c2ebee 100644 --- a/web/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/web/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -1,4 +1,5 @@ import {Modal} from 'antd'; +import useKeyEvent, {Keys} from 'hooks/useKeyEvent'; interface IProps { isOpen: boolean; @@ -19,6 +20,8 @@ const ConfirmationModal = ({ okText = 'Delete', cancelText = 'Cancel', }: IProps) => { + useKeyEvent([Keys.Enter], onConfirm); + return ( { +const ResourceCardActions = ({id, shouldEdit = true, onDelete, onEdit, onDuplicate}: IProps) => { const {getIsAllowed} = useCustomization(); + const canEdit = getIsAllowed(Operation.Edit); - const onDeleteClick = useCallback( - ({domEvent}) => { - domEvent?.stopPropagation(); - onDelete(); - }, - [onDelete] - ); - - const onEditClick = useCallback( - ({domEvent}) => { - domEvent?.stopPropagation(); - onEdit(); - }, - [onEdit] + const onAction = useCallback( + action => + ({domEvent}: {domEvent: React.MouseEvent | React.KeyboardEvent}) => { + domEvent?.stopPropagation(); + action(); + }, + [] ); const menuItems = useMemo(() => { const defaultItems = [ + { + key: 'duplicate', + label: Duplicate, + onClick: onAction(onDuplicate), + disabled: !canEdit, + }, { key: 'delete', label: Delete, - onClick: onDeleteClick, + onClick: onAction(onDelete), disabled: !getIsAllowed(Operation.Edit), }, ]; @@ -45,13 +46,13 @@ const ResourceCardActions = ({id, shouldEdit = true, onDelete, onEdit}: IProps) { key: 'edit', label: Edit, - onClick: onEditClick, + onClick: onAction(onEdit), disabled: !getIsAllowed(Operation.Edit), }, ...defaultItems, ] : defaultItems; - }, [getIsAllowed, onDeleteClick, onEditClick, shouldEdit]); + }, [canEdit, getIsAllowed, onAction, onDelete, onDuplicate, onEdit, shouldEdit]); return ( } placement="bottomLeft" trigger={['click']}> diff --git a/web/src/components/ResourceCard/TestCard.tsx b/web/src/components/ResourceCard/TestCard.tsx index f5276f1c39..49930702eb 100644 --- a/web/src/components/ResourceCard/TestCard.tsx +++ b/web/src/components/ResourceCard/TestCard.tsx @@ -18,11 +18,12 @@ interface IProps { onEdit(id: string, lastRunId: number, type: ResourceType): void; onDelete(id: string, name: string, type: ResourceType): void; onRun(test: Test, type: ResourceType): void; + onDuplicate(test: Test, type: ResourceType): void; onViewAll(id: string, type: ResourceType): void; test: Test; } -const TestCard = ({onEdit, onDelete, onRun, onViewAll, test}: IProps) => { +const TestCard = ({onEdit, onDelete, onDuplicate, onRun, onViewAll, test}: IProps) => { const queryParams = useMemo(() => ({take: 5, testId: test.id}), [test.id]); const {isCollapsed, isLoading, list, onClick} = useRuns( useLazyGetRunListQuery, @@ -65,6 +66,7 @@ const TestCard = ({onEdit, onDelete, onRun, onViewAll, test}: IProps) => { shouldEdit={shouldEdit} onDelete={() => onDelete(test.id, test.name, ResourceType.Test)} onEdit={() => onEdit(test.id, lastRunId, ResourceType.Test)} + onDuplicate={() => onDuplicate(test, ResourceType.Test)} /> diff --git a/web/src/components/ResourceCard/TestSuiteCard.tsx b/web/src/components/ResourceCard/TestSuiteCard.tsx index 2af44ce413..a2d59c5ef5 100644 --- a/web/src/components/ResourceCard/TestSuiteCard.tsx +++ b/web/src/components/ResourceCard/TestSuiteCard.tsx @@ -19,6 +19,7 @@ interface IProps { onEdit(id: string, lastRunId: number, type: ResourceType): void; onDelete(id: string, name: string, type: ResourceType): void; onRun(testSuite: TestSuite, type: ResourceType): void; + onDuplicate(testSuite: TestSuite): void; onViewAll(id: string, type: ResourceType): void; testSuite: TestSuite; } @@ -27,6 +28,7 @@ const TestSuiteCard = ({ onEdit, onDelete, onRun, + onDuplicate, onViewAll, testSuite: {id: testSuiteId, summary, name, description}, testSuite, @@ -71,6 +73,7 @@ const TestSuiteCard = ({ shouldEdit={shouldEdit} onDelete={() => onDelete(testSuiteId, name, ResourceType.TestSuite)} onEdit={() => onEdit(testSuiteId, lastRunId, ResourceType.TestSuite)} + onDuplicate={() => onDuplicate(testSuite)} /> diff --git a/web/src/components/ResourceCard/__tests__/ResourceCardActions.test.tsx b/web/src/components/ResourceCard/__tests__/ResourceCardActions.test.tsx index 35eefe1c7e..aa88b97b49 100644 --- a/web/src/components/ResourceCard/__tests__/ResourceCardActions.test.tsx +++ b/web/src/components/ResourceCard/__tests__/ResourceCardActions.test.tsx @@ -8,6 +8,14 @@ test('ResourceCardActions', async () => { const onDelete = jest.fn(); const testId = faker.datatype.uuid(); - const {getByTestId} = render(); + const {getByTestId} = render( + + ); await waitFor(() => getByTestId(`test-actions-button-${testId}`)); }); diff --git a/web/src/components/ResourceCard/__tests__/TestCard.test.tsx b/web/src/components/ResourceCard/__tests__/TestCard.test.tsx index bb7acb9843..9010e0a447 100644 --- a/web/src/components/ResourceCard/__tests__/TestCard.test.tsx +++ b/web/src/components/ResourceCard/__tests__/TestCard.test.tsx @@ -10,7 +10,14 @@ test('TestCard', async () => { const test = TestMock.model(); const {getByTestId, getByText} = render( - + ); const mouseEvent = new MouseEvent('click', {bubbles: true}); fireEvent(getByTestId(`test-actions-button-${test.id}`), mouseEvent); diff --git a/web/src/components/TestHeader/TestHeader.tsx b/web/src/components/TestHeader/TestHeader.tsx index 42906a53c4..e483a24a85 100644 --- a/web/src/components/TestHeader/TestHeader.tsx +++ b/web/src/components/TestHeader/TestHeader.tsx @@ -9,11 +9,12 @@ interface IProps { shouldEdit: boolean; onEdit(): void; onDelete(): void; + onDuplicate(): void; title: string; runButton: React.ReactElement; } -const TestHeader = ({description, id, shouldEdit, onEdit, onDelete, title, runButton}: IProps) => { +const TestHeader = ({description, id, shouldEdit, onEdit, onDelete, onDuplicate, title, runButton}: IProps) => { const {navigate} = useDashboard(); return ( @@ -30,7 +31,13 @@ const TestHeader = ({description, id, shouldEdit, onEdit, onDelete, title, runBu {runButton} - + ); diff --git a/web/src/components/TestHeader/__tests__/TestHeader.test.tsx b/web/src/components/TestHeader/__tests__/TestHeader.test.tsx index 994a5bc931..20c9ba3199 100644 --- a/web/src/components/TestHeader/__tests__/TestHeader.test.tsx +++ b/web/src/components/TestHeader/__tests__/TestHeader.test.tsx @@ -10,7 +10,16 @@ test('SpanAttributesTable', () => { const shouldEdit = true; const {getByTestId} = render( - } /> + } + onDuplicate={jest.fn()} + /> ); expect(getByTestId('test-details-name')).toBeInTheDocument(); }); diff --git a/web/src/hooks/useKeyEvent.ts b/web/src/hooks/useKeyEvent.ts new file mode 100644 index 0000000000..4aac03823c --- /dev/null +++ b/web/src/hooks/useKeyEvent.ts @@ -0,0 +1,34 @@ +import {useCallback, useEffect} from 'react'; + +type TEvent = 'keydown' | 'keypress' | 'keyup'; + +export enum Keys { + Enter = 'Enter', + Escape = 'Escape', +} + +const useKeyEvent = (targetKey: string[], callback: () => void, events: TEvent[] = ['keydown']) => { + const onEvent = useCallback( + (e: KeyboardEvent) => { + if (targetKey.includes(e.key)) { + e.stopPropagation(); + callback(); + } + }, + [callback, targetKey] + ); + + useEffect(() => { + events.forEach(event => { + window.addEventListener(event, onEvent); + }); + + return () => { + events.forEach(event => { + window.removeEventListener(event, onEvent); + }); + }; + }, [events, onEvent]); +}; + +export default useKeyEvent; diff --git a/web/src/models/TestOutput.model.ts b/web/src/models/TestOutput.model.ts index 39fcfa001e..77c4294fdd 100644 --- a/web/src/models/TestOutput.model.ts +++ b/web/src/models/TestOutput.model.ts @@ -14,13 +14,13 @@ type TestOutput = { error: string; }; -function TestOutput({name = '', selectorParsed = {}, value = ''}: TRawTestOutput, id = -1): TestOutput { +function TestOutput({name = '', selectorParsed = {}, selector = '', value = ''}: TRawTestOutput, id = -1): TestOutput { return { id, isDeleted: false, isDraft: false, name, - selector: selectorParsed.query || '', + selector: selectorParsed.query || selector || '', value, valueRun: '', valueRunDraft: '', diff --git a/web/src/pages/Home/TestsList.tsx b/web/src/pages/Home/TestsList.tsx index f267d46c6f..97b376460f 100644 --- a/web/src/pages/Home/TestsList.tsx +++ b/web/src/pages/Home/TestsList.tsx @@ -14,6 +14,7 @@ import {useDashboard} from 'providers/Dashboard/Dashboard.provider'; import VariableSetSelector from 'components/VariableSetSelector/VariableSetSelector'; import Test from 'models/Test.model'; import ImportModal from 'components/ImportModal/ImportModal'; +import {useConfirmationModal} from 'providers/ConfirmationModal/ConfirmationModal.provider'; import * as S from './Home.styled'; import HomeFilters from './HomeFilters'; import Loading from './Loading'; @@ -29,12 +30,13 @@ const Tests = () => { const [isCreateTestOpen, setIsCreateTestOpen] = useState(false); const [isImportTestOpen, setIsImportTestOpen] = useState(false); const [parameters, setParameters] = useState(defaultSort); + const {onOpen} = useConfirmationModal(); const pagination = usePagination(useGetTestListQuery, { ...parameters, }); const onDelete = useDeleteResource(); - const {runTest} = useTestCrud(); + const {runTest, duplicate} = useTestCrud(); const {navigate} = useDashboard(); const handleOnRun = useCallback( @@ -44,6 +46,18 @@ const Tests = () => { [runTest] ); + const handleOnDuplicate = useCallback( + (test: Test) => { + onOpen({ + heading: `Duplicate Test`, + title: `Create a duplicated version of Test: ${test.name}`, + okText: 'Duplicate', + onConfirm: () => duplicate(test), + }); + }, + [duplicate, onOpen] + ); + const handleOnViewAll = useCallback((id: string) => { onTestClick(id); }, []); @@ -106,6 +120,7 @@ const Tests = () => { onDelete={onDelete} onRun={handleOnRun} onViewAll={handleOnViewAll} + onDuplicate={handleOnDuplicate} test={test} /> ))} diff --git a/web/src/pages/Test/Content.tsx b/web/src/pages/Test/Content.tsx index 08b70f0e0a..a1dc8d2f36 100644 --- a/web/src/pages/Test/Content.tsx +++ b/web/src/pages/Test/Content.tsx @@ -1,4 +1,4 @@ -import {useMemo} from 'react'; +import {useCallback, useMemo} from 'react'; import CreateButton from 'components/CreateButton'; import PaginatedList from 'components/PaginatedList'; import TestRunCard from 'components/RunCard/TestRunCard'; @@ -10,6 +10,7 @@ import {useDashboard} from 'providers/Dashboard/Dashboard.provider'; import {useTest} from 'providers/Test/Test.provider'; import useTestCrud from 'providers/Test/hooks/useTestCrud'; import TracetestAPI from 'redux/apis/Tracetest'; +import {useConfirmationModal} from 'providers/ConfirmationModal/ConfirmationModal.provider'; import {ResourceType} from 'types/Resource.type'; import * as S from './Test.styled'; @@ -18,7 +19,8 @@ const {useGetRunListQuery} = TracetestAPI.instance; const Content = () => { const {test} = useTest(); const onDeleteResource = useDeleteResource(); - const {runTest, isLoadingRunTest} = useTestCrud(); + const {runTest, isLoadingRunTest, duplicate} = useTestCrud(); + const {onOpen} = useConfirmationModal(); const params = useMemo(() => ({testId: test.id}), [test.id]); useDocumentTitle(`${test.name}`); @@ -27,6 +29,15 @@ const Content = () => { const shouldEdit = test.summary.hasRuns; const onEdit = () => navigate(`/test/${test.id}/run/${test.summary.runs}`); + const handleOnDuplicate = useCallback(() => { + onOpen({ + heading: `Duplicate Test`, + title: `Create a duplicated version of Test: ${test.name}`, + okText: 'Duplicate', + onConfirm: () => duplicate(test), + }); + }, [duplicate, onOpen, test]); + return ( { id={test.id} onDelete={() => onDeleteResource(test.id, test.name, ResourceType.Test)} onEdit={onEdit} + onDuplicate={handleOnDuplicate} shouldEdit={shouldEdit} title={`${test.name} (v${test.version})`} runButton={ diff --git a/web/src/pages/TestSuite/Content.tsx b/web/src/pages/TestSuite/Content.tsx index 086d14ef22..81014578f1 100644 --- a/web/src/pages/TestSuite/Content.tsx +++ b/web/src/pages/TestSuite/Content.tsx @@ -8,6 +8,7 @@ import TestSuiteRun from 'models/TestSuiteRun.model'; import {useDashboard} from 'providers/Dashboard/Dashboard.provider'; import {useTestSuite} from 'providers/TestSuite/TestSuite.provider'; import {useTestSuiteCrud} from 'providers/TestSuite'; +import {useConfirmationModal} from 'providers/ConfirmationModal/ConfirmationModal.provider'; import TracetestAPI from 'redux/apis/Tracetest'; import * as S from './TestSuite.styled'; @@ -15,7 +16,8 @@ const {useGetTestSuiteRunsQuery} = TracetestAPI.instance; const Content = () => { const {onDelete, testSuite} = useTestSuite(); - const {runTestSuite, isEditLoading} = useTestSuiteCrud(); + const {runTestSuite, duplicate, isEditLoading} = useTestSuiteCrud(); + const {onOpen} = useConfirmationModal(); const params = useMemo(() => ({testSuiteId: testSuite.id}), [testSuite.id]); useDocumentTitle(`${testSuite.name}`); @@ -29,6 +31,15 @@ const Content = () => { const shouldEdit = testSuite.summary.hasRuns; const onEdit = () => navigate(`/testsuite/${testSuite.id}/run/${testSuite.summary.runs}`); + const handleOnDuplicate = useCallback(() => { + onOpen({ + heading: `Duplicate Test Suite`, + title: `Create a duplicated version of Test Suite: ${suite.name}`, + okText: 'Duplicate', + onConfirm: () => duplicate(testSuite), + }); + }, [duplicate, onOpen, testSuite]); + return ( { id={testSuite.id} onDelete={() => onDelete(testSuite.id, testSuite.name)} onEdit={onEdit} + onDuplicate={handleOnDuplicate} shouldEdit={shouldEdit} title={`${testSuite.name} (v${testSuite.version})`} runButton={ diff --git a/web/src/pages/TestSuites/TestSuitesList.tsx b/web/src/pages/TestSuites/TestSuitesList.tsx index ba55a7770c..3f087cc9de 100644 --- a/web/src/pages/TestSuites/TestSuitesList.tsx +++ b/web/src/pages/TestSuites/TestSuitesList.tsx @@ -14,6 +14,7 @@ import HomeAnalyticsService from 'services/Analytics/HomeAnalytics.service'; import {useDashboard} from 'providers/Dashboard/Dashboard.provider'; import useTestSuiteCrud from 'providers/TestSuite/hooks/useTestSuiteCrud'; import VariableSetSelector from 'components/VariableSetSelector/VariableSetSelector'; +import {useConfirmationModal} from 'providers/ConfirmationModal/ConfirmationModal.provider'; import TestSuite from 'models/TestSuite.model'; import * as S from './TestSuites.styled'; import HomeFilters from '../Home/HomeFilters'; @@ -32,8 +33,9 @@ const Resources = () => { const {data: testListData} = useGetTestListQuery({}); const pagination = usePagination(useGetTestSuiteListQuery, parameters); const onDelete = useDeleteResource(); - const {runTestSuite} = useTestSuiteCrud(); + const {runTestSuite, duplicate} = useTestSuiteCrud(); const {navigate} = useDashboard(); + const {onOpen} = useConfirmationModal(); const handleOnRun = useCallback( (resource: TestSuite) => { @@ -53,6 +55,18 @@ const Resources = () => { [navigate] ); + const handleOnDuplicate = useCallback( + (suite: TestSuite) => { + onOpen({ + heading: `Duplicate Test Suite`, + title: `Create a duplicated version of Test Suite: ${suite.name}`, + okText: 'Duplicate', + onConfirm: () => duplicate(suite), + }); + }, + [duplicate, onOpen] + ); + return ( <> @@ -126,6 +140,7 @@ const Resources = () => { onEdit={handleOnEdit} onDelete={onDelete} onRun={handleOnRun} + onDuplicate={handleOnDuplicate} onViewAll={handleOnViewAll} testSuite={suite} /> diff --git a/web/src/providers/CreateTest/CreateTest.provider.tsx b/web/src/providers/CreateTest/CreateTest.provider.tsx index 634b7bf692..27e2b7b658 100644 --- a/web/src/providers/CreateTest/CreateTest.provider.tsx +++ b/web/src/providers/CreateTest/CreateTest.provider.tsx @@ -1,13 +1,10 @@ import {noop} from 'lodash'; import useTestCrud from 'providers/Test/hooks/useTestCrud'; import {createContext, useCallback, useContext, useMemo, useState} from 'react'; -import TracetestAPI from 'redux/apis/Tracetest'; import TestService from 'services/Test.service'; import {IPlugin} from 'types/Plugins.types'; import {TDraftTest} from 'types/Test.types'; -const {useCreateTestMutation} = TracetestAPI.instance; - interface IContext { initialValues: TDraftTest; isLoading: boolean; @@ -30,16 +27,14 @@ interface IProps { const CreateTestProvider = ({children}: IProps) => { const [initialValues, setInitialValues] = useState({name: 'Untitled'}); - const [createTest, {isLoading: isLoadingCreateTest}] = useCreateTestMutation(); - const {runTest, isEditLoading: isLoadingEditTest} = useTestCrud(); + const {create, isLoadingCreateTest, isEditLoading: isLoadingEditTest} = useTestCrud(); const onCreateTest = useCallback( async (draft: TDraftTest, plugin: IPlugin) => { const rawTest = await TestService.getRequest(plugin, draft); - const test = await createTest(rawTest).unwrap(); - runTest({test}); + await create(rawTest); }, - [createTest, runTest] + [create] ); const onInitialValues = useCallback(values => setInitialValues(values), []); diff --git a/web/src/providers/CreateTestSuite/CreateTestSuite.provider.tsx b/web/src/providers/CreateTestSuite/CreateTestSuite.provider.tsx index e7ee732bcd..ed6221d6a6 100644 --- a/web/src/providers/CreateTestSuite/CreateTestSuite.provider.tsx +++ b/web/src/providers/CreateTestSuite/CreateTestSuite.provider.tsx @@ -1,6 +1,5 @@ import {noop} from 'lodash'; import {createContext, useCallback, useContext, useMemo} from 'react'; -import TracetestAPI from 'redux/apis/Tracetest'; import {useAppDispatch, useAppSelector} from 'redux/hooks'; import {initialState, reset, setDraft, setIsFormValid, setStepNumber} from 'redux/slices/CreateTestSuite.slice'; import CreateTestSuitesSelectors from 'selectors/CreateTestSuite.selectors'; @@ -8,8 +7,6 @@ import {ICreateTestStep} from 'types/Plugins.types'; import {ICreateTestSuiteState, TDraftTestSuite} from 'types/TestSuite.types'; import {useTestSuiteCrud} from '../TestSuite'; -const {useCreateTestSuiteMutation} = TracetestAPI.instance; - interface IContext extends ICreateTestSuiteState { isLoading: boolean; stepList: ICreateTestStep[]; @@ -43,8 +40,7 @@ export const useCreateTestSuite = () => useContext(Context); const CreateTestSuiteProvider = ({children}: IProps) => { const dispatch = useAppDispatch(); - const [create, {isLoading: isLoadingCreate}] = useCreateTestSuiteMutation(); - const {runTestSuite, isEditLoading} = useTestSuiteCrud(); + const {isEditLoading, create, isLoadingCreate} = useTestSuiteCrud(); const draft = useAppSelector(CreateTestSuitesSelectors.selectDraft); const stepNumber = useAppSelector(CreateTestSuitesSelectors.selectStepNumber); @@ -55,10 +51,9 @@ const CreateTestSuiteProvider = ({children}: IProps) => { const onCreate = useCallback( async (values: TDraftTestSuite) => { - const suite = await create(values).unwrap(); - runTestSuite(suite); + await create(values); }, - [create, runTestSuite] + [create] ); const onUpdate = useCallback( diff --git a/web/src/providers/Test/hooks/useTestCrud.ts b/web/src/providers/Test/hooks/useTestCrud.ts index 47776143cc..87624e57c5 100644 --- a/web/src/providers/Test/hooks/useTestCrud.ts +++ b/web/src/providers/Test/hooks/useTestCrud.ts @@ -5,7 +5,7 @@ import {TriggerTypeToPlugin} from 'constants/Plugins.constants'; import {TriggerTypes} from 'constants/Test.constants'; import {TVariableSetValue} from 'models/VariableSet.model'; import RunError from 'models/RunError.model'; -import Test from 'models/Test.model'; +import Test, {TRawTestResource} from 'models/Test.model'; import {useDashboard} from 'providers/Dashboard/Dashboard.provider'; import {useVariableSet} from 'providers/VariableSet'; import {useMissingVariablesModal} from 'providers/MissingVariablesModal/MissingVariablesModal.provider'; @@ -18,7 +18,7 @@ import TestService from 'services/Test.service'; import {TDraftTest} from 'types/Test.types'; import {RunErrorTypes} from 'types/TestRun.types'; -const {useEditTestMutation, useRunTestMutation} = TracetestAPI.instance; +const {useEditTestMutation, useRunTestMutation, useCreateTestMutation} = TracetestAPI.instance; export type TTestRunRequest = { test: Test; @@ -33,6 +33,7 @@ const useTestCrud = () => { const {updateIsInitialized} = useTestSpecs(); const [editTest, {isLoading: isLoadingEditTest}] = useEditTestMutation(); const [runTestAction, {isLoading: isLoadingRunTest}] = useRunTestMutation(); + const [createTest, {isLoading: isLoadingCreateTest}] = useCreateTestMutation(); const isEditLoading = isLoadingEditTest || isLoadingRunTest; const {mode = 'trigger'} = useParams(); @@ -88,11 +89,31 @@ const useTestCrud = () => { [editTest, runTest, updateIsInitialized] ); + const create = useCallback( + async (rawTest: TRawTestResource) => { + const test = await createTest(rawTest).unwrap(); + runTest({test}); + }, + [createTest, runTest] + ); + + const duplicate = useCallback( + async (test: Test) => { + const rawTest = await TestService.getDuplicatedRawTest(test, `${test.name} (copy)`); + + await create(rawTest); + }, + [create] + ); + return { edit, + create, runTest, + duplicate, isEditLoading, isLoadingRunTest, + isLoadingCreateTest, }; }; diff --git a/web/src/providers/TestSuite/hooks/useTestSuiteCrud.ts b/web/src/providers/TestSuite/hooks/useTestSuiteCrud.ts index 73493080cc..801c80cade 100644 --- a/web/src/providers/TestSuite/hooks/useTestSuiteCrud.ts +++ b/web/src/providers/TestSuite/hooks/useTestSuiteCrud.ts @@ -8,12 +8,14 @@ import {useMissingVariablesModal} from 'providers/MissingVariablesModal/MissingV import TracetestAPI from 'redux/apis/Tracetest'; import {RunErrorTypes} from 'types/TestRun.types'; import {TDraftTestSuite} from 'types/TestSuite.types'; +import TestSuiteService from 'services/TestSuite.service'; const { useEditTestSuiteMutation, useRunTestSuiteMutation, useLazyGetTestSuiteVersionByIdQuery, useDeleteTestSuiteByIdMutation, + useCreateTestSuiteMutation, } = TracetestAPI.instance; const useTestSuiteCrud = () => { @@ -22,6 +24,7 @@ const useTestSuiteCrud = () => { const [runTestSuiteAction, {isLoading: isLoadingRunTestSuite}] = useRunTestSuiteMutation(); const [getTestSuite] = useLazyGetTestSuiteVersionByIdQuery(); const [deleteTestSuiteAction] = useDeleteTestSuiteByIdMutation(); + const [createAction, {isLoading: isLoadingCreate}] = useCreateTestSuiteMutation(); const isEditLoading = isTestSuiteEditLoading || isLoadingRunTestSuite; const {selectedVariableSet} = useVariableSet(); const {onOpen} = useMissingVariablesModal(); @@ -76,11 +79,31 @@ const useTestSuiteCrud = () => { [deleteTestSuiteAction, navigate] ); + const create = useCallback( + async (values: TDraftTestSuite) => { + const suite = await createAction(values).unwrap(); + runTestSuite(suite); + }, + [createAction, runTestSuite] + ); + + const duplicate = useCallback( + async (suite: TestSuite) => { + const draft = TestSuiteService.getDuplicatedDraftTestSuite(suite, `${suite.name} (copy)`); + + await create(draft); + }, + [create] + ); + return { edit, runTestSuite, deleteTestSuite, + duplicate, + create, isEditLoading, + isLoadingCreate, }; }; diff --git a/web/src/services/Test.service.ts b/web/src/services/Test.service.ts index 8df15cdb7b..b59d772c33 100644 --- a/web/src/services/Test.service.ts +++ b/web/src/services/Test.service.ts @@ -97,6 +97,11 @@ const TestService = () => ({ const updatedTest = {...test, ...partialTest}; return this.getRequest(plugin, testTriggerData, updatedTest); }, + + async getDuplicatedRawTest(test: Test, name: string): Promise { + const raw = await this.getUpdatedRawTest(test, {}); + return {...raw, spec: {...raw.spec, name}}; + }, }); export default TestService(); diff --git a/web/src/services/TestSuite.service.ts b/web/src/services/TestSuite.service.ts index 1eab1f70e3..0b0371bcb5 100644 --- a/web/src/services/TestSuite.service.ts +++ b/web/src/services/TestSuite.service.ts @@ -15,6 +15,12 @@ const TestSuiteService = () => ({ steps: suite.fullSteps.map(step => step.id), }; }, + + getDuplicatedDraftTestSuite(suite: TestSuite, name: string): TDraftTestSuite { + const draft = this.getInitialValues(suite); + + return {...draft, name, id: undefined, version: undefined}; + }, }); export default TestSuiteService(); diff --git a/web/src/types/TestSuite.types.ts b/web/src/types/TestSuite.types.ts index ba7054a9d0..b27f1e8f29 100644 --- a/web/src/types/TestSuite.types.ts +++ b/web/src/types/TestSuite.types.ts @@ -6,6 +6,8 @@ export type TDraftTestSuite = { steps?: string[]; name?: string; description?: string; + id?: string; + version?: number; }; export interface ICreateTestSuiteState {