diff --git a/frontend/__tests__/constants/fileConfig/fileConfig.test.ts b/frontend/__tests__/constants/fileConfig/fileConfig.test.ts index 722ac3d961..e0ee8726fa 100644 --- a/frontend/__tests__/constants/fileConfig/fileConfig.test.ts +++ b/frontend/__tests__/constants/fileConfig/fileConfig.test.ts @@ -5,7 +5,7 @@ import { CHINA_CALENDAR, DEFAULT_REWORK_SETTINGS, } from '../../fixtures'; -import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { convertToNewFileConfig } from '@src/constants/fileConfig'; describe('#fileConfig', () => { diff --git a/frontend/__tests__/containers/ConfigStep/MetricsTypeCheckbox.test.tsx b/frontend/__tests__/containers/ConfigStep/BasicInfo.test.tsx similarity index 84% rename from frontend/__tests__/containers/ConfigStep/MetricsTypeCheckbox.test.tsx rename to frontend/__tests__/containers/ConfigStep/BasicInfo.test.tsx index 2a46a935b4..f505be840b 100644 --- a/frontend/__tests__/containers/ConfigStep/MetricsTypeCheckbox.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/BasicInfo.test.tsx @@ -2,7 +2,6 @@ import { ALL, DEV_CHANGE_FAILURE_RATE, CLASSIFICATION, - CONFIG_TITLE, CYCLE_TIME, DEPLOYMENT_FREQUENCY, LEAD_TIME_FOR_CHANGES, @@ -12,11 +11,13 @@ import { REWORK_TIMES, VELOCITY, } from '../../fixtures'; -import { MetricsTypeCheckbox } from '@src/containers/ConfigStep/MetricsTypeCheckbox'; +import { basicInfoDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; +import { basicInfoSchema } from '@src/containers/ConfigStep/Form/schema'; import { render, waitFor, within, screen } from '@testing-library/react'; import { SELECTED_VALUE_SEPARATOR } from '@src/constants/commons'; import BasicInfo from '@src/containers/ConfigStep/BasicInfo'; import { setupStore } from '../../utils/setupStoreUtil'; +import { FormProvider } from '@test/utils/FormProvider'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; @@ -27,8 +28,9 @@ describe('MetricsTypeCheckbox', () => { store = setupStore(); return render( - - + + + , ); }; @@ -146,23 +148,4 @@ describe('MetricsTypeCheckbox', () => { expect(getByText(/Metrics is required/i)).toBeInTheDocument(); }); - - it('should show board component when click MetricsTypeCheckbox selection velocity ', async () => { - setup(); - await userEvent.click(screen.getByRole('combobox', { name: REQUIRED_DATA })); - const listBox = within(screen.getByRole('listbox')); - await userEvent.click(listBox.getByRole('option', { name: VELOCITY })); - expect(screen.getAllByText(CONFIG_TITLE.BOARD)[0]).toBeInTheDocument(); - }); - - it('should hidden board component when MetricsTypeCheckbox select is null given MetricsTypeCheckbox select is velocity ', async () => { - setup(); - - await userEvent.click(screen.getByRole('combobox', { name: REQUIRED_DATA })); - const requireDateSelection = within(screen.getByRole('listbox')); - await userEvent.click(requireDateSelection.getByRole('option', { name: VELOCITY })); - await userEvent.click(requireDateSelection.getByRole('option', { name: VELOCITY })); - - expect(screen.queryByText(CONFIG_TITLE.BOARD)).not.toBeInTheDocument(); - }); }); diff --git a/frontend/__tests__/containers/ConfigStep/Board.test.tsx b/frontend/__tests__/containers/ConfigStep/Board.test.tsx index 85f092ed0d..5dd859cbed 100644 --- a/frontend/__tests__/containers/ConfigStep/Board.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/Board.test.tsx @@ -10,11 +10,14 @@ import { FAKE_TOKEN, REVERIFY, } from '../../fixtures'; +import { boardConfigDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; +import { boardConfigSchema } from '@src/containers/ConfigStep/Form/schema'; import { render, screen, waitFor, within } from '@testing-library/react'; import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources'; import { boardClient } from '@src/clients/board/BoardClient'; import { Board } from '@src/containers/ConfigStep/Board'; import { setupStore } from '../../utils/setupStoreUtil'; +import { FormProvider } from '@test/utils/FormProvider'; import { TimeoutError } from '@src/errors/TimeoutError'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; @@ -59,7 +62,9 @@ describe('Board', () => { store = setupStore(); return render( - + + + , ); }; @@ -252,4 +257,39 @@ describe('Board', () => { ).toBeInTheDocument(); }); }); + + it('should close alert modal when user manually close the alert', async () => { + setup(); + await fillBoardFieldsInformation(); + const timeoutError = new TimeoutError('', AXIOS_REQUEST_ERROR_CODE.TIMEOUT); + boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(timeoutError)); + + await userEvent.click(screen.getByText(VERIFY)); + + expect(screen.getByTestId('timeoutAlert')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Close')); + + expect(screen.queryByLabelText('timeoutAlert')).not.toBeInTheDocument(); + }); + + it('should allow user to re-submit when user interact again with form given form is already submit successfully', async () => { + setup(); + mockVerifySuccess(); + await fillBoardFieldsInformation(); + + expect(screen.getByRole('button', { name: /verify/i })).toBeEnabled(); + + await userEvent.click(screen.getByText(/verify/i)); + + expect(await screen.findByRole('button', { name: /reset/i })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /verified/i })).toBeDisabled(); + + const emailInput = (await screen.findByRole('textbox', { name: 'Email' })) as HTMLInputElement; + await userEvent.clear(emailInput); + await userEvent.type(emailInput, 'other@qq.com'); + const verifyButton = await screen.findByRole('button', { name: /verify/i }); + + expect(verifyButton).toBeEnabled(); + }); }); diff --git a/frontend/__tests__/containers/ConfigStep/ConfigStep.test.tsx b/frontend/__tests__/containers/ConfigStep/ConfigStep.test.tsx index 012b813ac9..83d64fb636 100644 --- a/frontend/__tests__/containers/ConfigStep/ConfigStep.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/ConfigStep.test.tsx @@ -15,13 +15,33 @@ import { VELOCITY, VERIFIED, VERIFY, + ALL, + FAKE_TOKEN, + PIPELINE_TOOL_TOKEN_INPUT_LABEL, } from '../../fixtures'; -import { fillBoardFieldsInformation } from '@test/containers/ConfigStep/Board.test'; +import { + basicInfoSchema, + boardConfigSchema, + pipelineToolSchema, + sourceControlSchema, + IBasicInfoData, + IBoardConfigData, + IPipelineToolData, + ISourceControlData, +} from '@src/containers/ConfigStep/Form/schema'; +import { + basicInfoDefaultValues, + boardConfigDefaultValues, + pipelineToolDefaultValues, + sourceControlDefaultValues, +} from '@src/containers/ConfigStep/Form/useDefaultValues'; import { act, render, screen, waitFor, within } from '@testing-library/react'; import { setupStore } from '../../utils/setupStoreUtil'; +import { yupResolver } from '@hookform/resolvers/yup'; import userEvent from '@testing-library/user-event'; import ConfigStep from '@src/containers/ConfigStep'; import { closeMuiModal } from '@test/testUtils'; +import { useForm } from 'react-hook-form'; import { Provider } from 'react-redux'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; @@ -39,18 +59,59 @@ const server = setupServer( ), ); +export const fillBoardFieldsInformation = async () => { + await userEvent.type(screen.getByLabelText(/board id/i), '1'); + await userEvent.type(screen.getByLabelText(/email/i), 'fake@qq.com'); + await userEvent.type(screen.getByLabelText(/site/i), 'fake'); + await userEvent.type(screen.getByLabelText(/token/i), FAKE_TOKEN); +}; + let store = null; jest.mock('@src/context/config/configSlice', () => ({ ...jest.requireActual('@src/context/config/configSlice'), selectWarningMessage: jest.fn().mockReturnValue('Test warning Message'), })); +const ConfigStepWithFormInstances = () => { + const basicInfoMethods = useForm({ + defaultValues: basicInfoDefaultValues, + resolver: yupResolver(basicInfoSchema), + mode: 'onChange', + }); + + const boardConfigMethods = useForm({ + defaultValues: boardConfigDefaultValues, + resolver: yupResolver(boardConfigSchema), + mode: 'onChange', + }); + + const pipelineToolMethods = useForm({ + defaultValues: pipelineToolDefaultValues, + resolver: yupResolver(pipelineToolSchema), + mode: 'onChange', + }); + + const sourceControlMethods = useForm({ + defaultValues: sourceControlDefaultValues, + resolver: yupResolver(sourceControlSchema), + mode: 'onChange', + }); + return ( + + ); +}; + describe('ConfigStep', () => { const setup = () => { store = setupStore(); return render( - + , ); }; @@ -235,7 +296,9 @@ describe('ConfigStep', () => { const requireDateSelection = within(screen.getByRole('listbox')); await userEvent.click(requireDateSelection.getByRole('option', { name: DEPLOYMENT_FREQUENCY })); await closeMuiModal(userEvent); - const tokenNode = within(screen.getByTestId('pipelineToolTextField')).getByLabelText('input Token'); + const tokenNode = within(screen.getByTestId('pipelineToolTextField')).getByLabelText( + PIPELINE_TOOL_TOKEN_INPUT_LABEL, + ); await userEvent.type(tokenNode, FAKE_PIPELINE_TOKEN); const submitButton = screen.getByText(VERIFY); await userEvent.click(submitButton); @@ -248,4 +311,18 @@ describe('ConfigStep', () => { expect(screen.queryByText(VERIFIED)).toBeVisible(); expect(screen.queryByText(RESET)).toBeVisible(); }); + + it('should show all forms given all metrics selected', async () => { + setup(); + + const requiredMetricsField = screen.getByRole('combobox', { name: REQUIRED_DATA }); + await userEvent.click(requiredMetricsField); + const requireDateSelection = within(screen.getByRole('listbox')); + await userEvent.click(requireDateSelection.getByRole('option', { name: ALL })); + await closeMuiModal(userEvent); + + expect(screen.getByLabelText('Board Config')).toBeInTheDocument(); + expect(screen.getByLabelText('Pipeline Tool Config')).toBeInTheDocument(); + expect(screen.getByLabelText('Source Control Config')).toBeInTheDocument(); + }); }); diff --git a/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx b/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx index 8d44fde57c..b209ed96e3 100644 --- a/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx @@ -1,15 +1,18 @@ import { updateShouldGetBoardConfig, updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; +import { basicInfoDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; import { DateRangePickerSection } from '@src/containers/ConfigStep/DateRangePicker'; +import { basicInfoSchema } from '@src/containers/ConfigStep/Form/schema'; import { ERROR_DATE, TIME_RANGE_ERROR_MESSAGE } from '../../fixtures'; import { render, screen, within } from '@testing-library/react'; import { setupStore } from '../../utils/setupStoreUtil'; +import { FormProvider } from '@test/utils/FormProvider'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import React from 'react'; import dayjs from 'dayjs'; -const START_DATE_LABEL = 'From *'; -const END_DATE_LABEL = 'To *'; +const START_DATE_LABEL = 'From'; +const END_DATE_LABEL = 'To'; const TODAY = dayjs('2024-03-20'); const INPUT_DATE_VALUE = TODAY.format('MM/DD/YYYY'); let store = setupStore(); @@ -26,7 +29,9 @@ const setup = () => { store = setupStore(); return render( - + + + , ); }; @@ -300,4 +305,46 @@ describe('DateRangePickerSection', () => { await userEvent.click(sortButton); expect(screen.getByRole('button', { name: 'Ascending' })).toBeInTheDocument(); }); + + it('should provide unified error message when given all invalid time input', async () => { + const correctRange = ['03/15/2024', '03/25/2024']; + const rangeOfTooEarly = ['03/15/1600', '03/25/1600']; + const rangeOfInvalidFormat = ['XXxYY/2024', 'ZZ/11/2024']; + const startDateRequiredErrorMessage = 'Start date is required'; + const endDateRequiredErrorMessage = 'End date is required'; + const unifiedStartDateErrorMessage = 'Start date is invalid'; + const unifiedEndDateErrorMessage = 'End date is invalid'; + + const ranges = screen.getAllByLabelText('Range picker row'); + const startDateInput = within(ranges[0]).getByRole('textbox', { name: START_DATE_LABEL }) as HTMLInputElement; + const endDateInput = within(ranges[0]).getByRole('textbox', { name: END_DATE_LABEL }) as HTMLInputElement; + await userEvent.type(startDateInput, rangeOfTooEarly[0]); + await userEvent.type(endDateInput, rangeOfTooEarly[1]); + + expect(await screen.findByText(unifiedStartDateErrorMessage)).toBeVisible(); + expect(await screen.findByText(unifiedEndDateErrorMessage)).toBeVisible(); + + await userEvent.clear(startDateInput); + await userEvent.clear(endDateInput); + await userEvent.keyboard('{Tab}'); + + expect(await screen.findByText(startDateRequiredErrorMessage)).toBeVisible(); + expect(await screen.findByText(endDateRequiredErrorMessage)).toBeVisible(); + + await userEvent.type(startDateInput, correctRange[0]); + await userEvent.type(endDateInput, correctRange[1]); + + expect(screen.queryByText(startDateRequiredErrorMessage)).toBeNull(); + expect(screen.queryByText(endDateRequiredErrorMessage)).toBeNull(); + expect(screen.queryByText(unifiedStartDateErrorMessage)).toBeNull(); + expect(screen.queryByText(unifiedEndDateErrorMessage)).toBeNull(); + + await userEvent.type(startDateInput, rangeOfInvalidFormat[0]); + await userEvent.type(endDateInput, rangeOfInvalidFormat[1]); + + expect(screen.queryByText(startDateRequiredErrorMessage)).toBeNull(); + expect(screen.queryByText(endDateRequiredErrorMessage)).toBeNull(); + expect(screen.queryByText(unifiedStartDateErrorMessage)).toBeVisible(); + expect(screen.queryByText(unifiedEndDateErrorMessage)).toBeVisible(); + }); }); diff --git a/frontend/__tests__/containers/ConfigStep/PipelineTool.test.tsx b/frontend/__tests__/containers/ConfigStep/PipelineTool.test.tsx index af1c886bf4..e83a5eac44 100644 --- a/frontend/__tests__/containers/ConfigStep/PipelineTool.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/PipelineTool.test.tsx @@ -10,12 +10,18 @@ import { MOCK_PIPELINE_VERIFY_URL, FAKE_PIPELINE_TOKEN, REVERIFY, + PIPELINE_TOOL_TOKEN_INPUT_LABEL, + TIMEOUT_ALERT_TEST_ID, } from '../../fixtures'; +import { pipelineToolDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; import { pipelineToolClient } from '@src/clients/pipeline/PipelineToolClient'; +import { pipelineToolSchema } from '@src/containers/ConfigStep/Form/schema'; import { render, screen, waitFor, within } from '@testing-library/react'; import { PipelineTool } from '@src/containers/ConfigStep/PipelineTool'; import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources'; import { setupStore } from '../../utils/setupStoreUtil'; +import { FormProvider } from '@test/utils/FormProvider'; +import { TimeoutError } from '@src/errors/TimeoutError'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import { setupServer } from 'msw/node'; @@ -24,7 +30,7 @@ import { rest } from 'msw'; export const fillPipelineToolFieldsInformation = async () => { const tokenInput = within(screen.getByTestId('pipelineToolTextField')).getByLabelText( - 'input Token', + PIPELINE_TOOL_TOKEN_INPUT_LABEL, ) as HTMLInputElement; await userEvent.type(tokenInput, FAKE_PIPELINE_TOKEN); @@ -40,19 +46,22 @@ const originalVerify = pipelineToolClient.verify; describe('PipelineTool', () => { beforeAll(() => server.listen()); afterAll(() => server.close()); + afterEach(() => { + store = null; + pipelineToolClient.verify = originalVerify; + }); + store = setupStore(); const setup = () => { store = setupStore(); return render( - + + + , ); }; - afterEach(() => { - store = null; - pipelineToolClient.verify = originalVerify; - }); it('should show pipelineTool title and fields when render pipelineTool component ', () => { setup(); @@ -74,7 +83,7 @@ describe('PipelineTool', () => { it('should clear all fields information when click reset button', async () => { setup(); const tokenInput = within(screen.getByTestId('pipelineToolTextField')).getByLabelText( - 'input Token', + PIPELINE_TOOL_TOKEN_INPUT_LABEL, ) as HTMLInputElement; await fillPipelineToolFieldsInformation(); @@ -95,11 +104,11 @@ describe('PipelineTool', () => { await userEvent.click(screen.getByText(VERIFY)); - expect(screen.getByTestId('timeoutAlert')).toBeInTheDocument(); + expect(screen.getByTestId(TIMEOUT_ALERT_TEST_ID)).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', { name: RESET })); - expect(screen.queryByTestId('timeoutAlert')).not.toBeInTheDocument(); + expect(screen.queryByTestId(TIMEOUT_ALERT_TEST_ID)).not.toBeInTheDocument(); }); it('should hidden timeout alert when the error type of api call becomes other', async () => { @@ -109,13 +118,13 @@ describe('PipelineTool', () => { await userEvent.click(screen.getByText(VERIFY)); - expect(screen.getByTestId('timeoutAlert')).toBeInTheDocument(); + expect(screen.getByTestId(TIMEOUT_ALERT_TEST_ID)).toBeInTheDocument(); pipelineToolClient.verify = jest.fn().mockResolvedValue({ code: HttpStatusCode.Unauthorized }); await userEvent.click(screen.getByText(REVERIFY)); - expect(screen.queryByTestId('timeoutAlert')).not.toBeInTheDocument(); + expect(screen.queryByTestId(TIMEOUT_ALERT_TEST_ID)).not.toBeInTheDocument(); }); it('should show detail options when click pipelineTool fields', async () => { @@ -144,7 +153,7 @@ describe('PipelineTool', () => { await fillPipelineToolFieldsInformation(); const mockInfo = 'mockToken'; const tokenInput = within(screen.getByTestId('pipelineToolTextField')).getByLabelText( - 'input Token', + PIPELINE_TOOL_TOKEN_INPUT_LABEL, ) as HTMLInputElement; await userEvent.type(tokenInput, mockInfo); await userEvent.clear(tokenInput); @@ -162,7 +171,7 @@ describe('PipelineTool', () => { it('should show error message when focus on field given an empty value', async () => { setup(); - await userEvent.click(screen.getByLabelText('input Token')); + await userEvent.click(screen.getByLabelText(PIPELINE_TOOL_TOKEN_INPUT_LABEL)); expect(screen.getByText(TOKEN_ERROR_MESSAGE[1])).toBeInTheDocument(); expect(screen.getByText(TOKEN_ERROR_MESSAGE[1])).toHaveStyle(ERROR_MESSAGE_COLOR); @@ -172,7 +181,7 @@ describe('PipelineTool', () => { setup(); const mockInfo = 'mockToken'; const tokenInput = within(screen.getByTestId('pipelineToolTextField')).getByLabelText( - 'input Token', + PIPELINE_TOOL_TOKEN_INPUT_LABEL, ) as HTMLInputElement; await userEvent.type(tokenInput, mockInfo); @@ -224,4 +233,41 @@ describe('PipelineTool', () => { expect(getByText('Token is incorrect!')).toBeInTheDocument(); }); }); + + it('should close alert modal when user manually close the alert', async () => { + setup(); + await fillPipelineToolFieldsInformation(); + const timeoutError = new TimeoutError('', AXIOS_REQUEST_ERROR_CODE.TIMEOUT); + pipelineToolClient.verify = jest.fn().mockImplementation(() => Promise.resolve(timeoutError)); + + await userEvent.click(screen.getByText(VERIFY)); + + expect(await screen.getByTestId(TIMEOUT_ALERT_TEST_ID)).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Close')); + + expect(screen.queryByTestId(TIMEOUT_ALERT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should allow user to re-submit when user interact again with form given form is already submit successfully', async () => { + server.use( + rest.post(MOCK_PIPELINE_VERIFY_URL, (_, res, ctx) => res(ctx.delay(100), ctx.status(HttpStatusCode.NoContent))), + ); + setup(); + await fillPipelineToolFieldsInformation(); + + expect(screen.getByRole('button', { name: /verify/i })).toBeEnabled(); + + await userEvent.click(screen.getByText(/verify/i)); + + expect(await screen.findByRole('button', { name: /reset/i })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /verified/i })).toBeDisabled(); + + const tokenInput = (await screen.findByLabelText('Token *')) as HTMLInputElement; + await userEvent.clear(tokenInput); + await userEvent.type(tokenInput, FAKE_PIPELINE_TOKEN); + const verifyButton = await screen.findByRole('button', { name: /verify/i }); + + expect(verifyButton).toBeEnabled(); + }); }); diff --git a/frontend/__tests__/containers/ConfigStep/SortingDateRange.test.tsx b/frontend/__tests__/containers/ConfigStep/SortingDateRange.test.tsx index 694e67a53d..e7e22fe33c 100644 --- a/frontend/__tests__/containers/ConfigStep/SortingDateRange.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/SortingDateRange.test.tsx @@ -1,5 +1,5 @@ import { SortingDateRange } from '@src/containers/ConfigStep/DateRangePicker/SortingDateRange'; -import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { updateDateRangeSortType } from '@src/context/config/configSlice'; import { setupStore } from '@test/utils/setupStoreUtil'; import { render, screen } from '@testing-library/react'; diff --git a/frontend/__tests__/containers/ConfigStep/SourceControl.test.tsx b/frontend/__tests__/containers/ConfigStep/SourceControl.test.tsx index 521508bd15..351ea399e0 100644 --- a/frontend/__tests__/containers/ConfigStep/SourceControl.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/SourceControl.test.tsx @@ -10,12 +10,15 @@ import { VERIFIED, VERIFY, } from '../../fixtures'; +import { sourceControlDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; import { AXIOS_REQUEST_ERROR_CODE, SOURCE_CONTROL_TYPES } from '@src/constants/resources'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { sourceControlSchema } from '@src/containers/ConfigStep/Form/schema'; import { SourceControl } from '@src/containers/ConfigStep/SourceControl'; +import { render, screen, act, waitFor } from '@testing-library/react'; import { setupStore } from '../../utils/setupStoreUtil'; +import { FormProvider } from '@test/utils/FormProvider'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import { setupServer } from 'msw/node'; @@ -23,15 +26,16 @@ import { HttpStatusCode } from 'axios'; import { rest } from 'msw'; import React from 'react'; -export const fillSourceControlFieldsInformation = () => { - const mockInfo = 'AAAAA_XXXXXX' - .replace('AAAAA', 'ghpghoghughsghr') - .replace('XXXXXX', '1A2b1A2b1A2b1A2b1A2b1A2b1A2b1A2b1A2b'); +const mockValidFormtToken = 'AAAAA_XXXXXX' + .replace('AAAAA', 'ghpghoghughsghr') + .replace('XXXXXX', '1A2b1A2b1A2b1A2b1A2b1A2b1A2b1A2b1A2b'); + +export const fillSourceControlFieldsInformation = async () => { const tokenInput = screen.getByTestId('sourceControlTextField').querySelector('input') as HTMLInputElement; - fireEvent.change(tokenInput, { target: { value: mockInfo } }); + await userEvent.type(tokenInput, mockValidFormtToken); - expect(tokenInput.value).toEqual(mockInfo); + expect(tokenInput.value).toEqual(mockValidFormtToken); }; let store = null; @@ -49,19 +53,22 @@ jest.mock('@src/context/Metrics/metricsSlice', () => ({ describe('SourceControl', () => { beforeAll(() => server.listen()); afterAll(() => server.close()); + afterEach(() => { + store = null; + sourceControlClient.verifyToken = originalVerifyToken; + }); + store = setupStore(); const setup = () => { store = setupStore(); return render( - + + + , ); }; - afterEach(() => { - store = null; - sourceControlClient.verifyToken = originalVerifyToken; - }); it('should show sourceControl title and fields when render sourceControl component', () => { setup(); @@ -83,9 +90,9 @@ describe('SourceControl', () => { setup(); const tokenInput = screen.getByTestId('sourceControlTextField').querySelector('input') as HTMLInputElement; - fillSourceControlFieldsInformation(); + await fillSourceControlFieldsInformation(); - await userEvent.click(screen.getByText(VERIFY)); + await userEvent.click(screen.getByRole('button', { name: VERIFY })); await waitFor(async () => { expect(screen.getByRole('button', { name: RESET })).toBeTruthy(); @@ -133,22 +140,24 @@ describe('SourceControl', () => { expect(queryByTestId('timeoutAlert')).not.toBeInTheDocument(); }); - it('should enable verify button when all fields checked correctly given disable verify button', () => { + it('should enable verify button when all fields checked correctly given disable verify button', async () => { setup(); const verifyButton = screen.getByRole('button', { name: VERIFY }); expect(verifyButton).toBeDisabled(); - fillSourceControlFieldsInformation(); + await fillSourceControlFieldsInformation(); - expect(verifyButton).toBeEnabled(); + await waitFor(() => { + expect(screen.getByRole('button', { name: VERIFY })).toBeEnabled(); + }); }); it('should show reset button and verified button when verify successfully', async () => { setup(); - fillSourceControlFieldsInformation(); + await fillSourceControlFieldsInformation(); - fireEvent.click(screen.getByText(VERIFY)); + await userEvent.click(screen.getByText(VERIFY)); await waitFor(() => { expect(screen.getByText(RESET)).toBeTruthy(); @@ -161,25 +170,24 @@ describe('SourceControl', () => { it('should reload pipeline config when reset fields', async () => { setup(); - fillSourceControlFieldsInformation(); + await fillSourceControlFieldsInformation(); await userEvent.click(screen.getByText(VERIFY)); await userEvent.click(screen.getByRole('button', { name: RESET })); - fillSourceControlFieldsInformation(); + await fillSourceControlFieldsInformation(); expect(updateShouldGetPipelineConfig).toHaveBeenCalledWith(true); }); - it('should show error message and error style when token is empty', () => { + it('should show error message and error style when token is empty', async () => { setup(); - fillSourceControlFieldsInformation(); - const tokenInput = screen.getByTestId('sourceControlTextField').querySelector('input') as HTMLInputElement; - - fireEvent.change(tokenInput, { target: { value: '' } }); + act(() => { + tokenInput.focus(); + }); expect(screen.getByText(TOKEN_ERROR_MESSAGE[1])).toBeInTheDocument(); expect(screen.getByText(TOKEN_ERROR_MESSAGE[1])).toHaveStyle(ERROR_MESSAGE_COLOR); @@ -191,21 +199,24 @@ describe('SourceControl', () => { expect(screen.queryByText(TOKEN_ERROR_MESSAGE[1])).not.toBeInTheDocument(); }); - it('should show error message when focus on field given an empty value', () => { + it('should show error message when focus on field given an empty value', async () => { setup(); - fireEvent.focus(screen.getByLabelText('input Token')); + const tokenInput = screen.getByTestId('sourceControlTextField').querySelector('input') as HTMLInputElement; + act(() => { + tokenInput.focus(); + }); expect(screen.getByText(TOKEN_ERROR_MESSAGE[1])).toBeInTheDocument(); expect(screen.getByText(TOKEN_ERROR_MESSAGE[1])).toHaveStyle(ERROR_MESSAGE_COLOR); }); - it('should show error message and error style when token is invalid', () => { + it('should show error message and error style when token is invalid', async () => { setup(); const mockInfo = 'mockToken'; const tokenInput = screen.getByTestId('sourceControlTextField').querySelector('input') as HTMLInputElement; - fireEvent.change(tokenInput, { target: { value: mockInfo } }); + await userEvent.type(tokenInput, mockInfo); expect(tokenInput.value).toEqual(mockInfo); expect(screen.getByText(TOKEN_ERROR_MESSAGE[0])).toBeInTheDocument(); @@ -218,12 +229,49 @@ describe('SourceControl', () => { ); setup(); - fillSourceControlFieldsInformation(); + await fillSourceControlFieldsInformation(); + await userEvent.click(screen.getByRole('button', { name: VERIFY })); - fireEvent.click(screen.getByRole('button', { name: VERIFY })); + expect(screen.getByText(MOCK_SOURCE_CONTROL_VERIFY_ERROR_CASE_TEXT)).toBeInTheDocument(); + }); - await waitFor(() => { - expect(screen.getByText(MOCK_SOURCE_CONTROL_VERIFY_ERROR_CASE_TEXT)).toBeInTheDocument(); + it('should close alert modal when user manually close the alert', async () => { + setup(); + await fillSourceControlFieldsInformation(); + sourceControlClient.verifyToken = jest.fn().mockResolvedValue({ + code: AXIOS_REQUEST_ERROR_CODE.TIMEOUT, }); + + await userEvent.click(screen.getByText(VERIFY)); + + expect(await screen.getByTestId('timeoutAlert')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Close')); + + expect(screen.queryByLabelText('timeoutAlert')).not.toBeInTheDocument(); + }); + + it('should allow user to re-submit when user interact again with form given form is already submit successfully', async () => { + server.use( + rest.post(MOCK_SOURCE_CONTROL_VERIFY_TOKEN_URL, (req, res, ctx) => + res(ctx.delay(100), ctx.status(HttpStatusCode.NoContent)), + ), + ); + setup(); + await fillSourceControlFieldsInformation(); + + expect(screen.getByRole('button', { name: /verify/i })).toBeEnabled(); + + await userEvent.click(screen.getByText(/verify/i)); + + expect(await screen.findByRole('button', { name: /reset/i })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /verified/i })).toBeDisabled(); + + const tokenInput = (await screen.findByLabelText('Token *')) as HTMLInputElement; + await userEvent.clear(tokenInput); + await userEvent.type(tokenInput, mockValidFormtToken); + const verifyButton = await screen.findByRole('button', { name: /verify/i }); + + expect(verifyButton).toBeEnabled(); }); }); diff --git a/frontend/__tests__/containers/ConfigStep/TimeoutAlet.test.tsx b/frontend/__tests__/containers/ConfigStep/TimeoutAlet.test.tsx index 986fdd3461..bb112b6730 100644 --- a/frontend/__tests__/containers/ConfigStep/TimeoutAlet.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/TimeoutAlet.test.tsx @@ -4,41 +4,20 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; describe('TimeoutAlert', () => { - const setIsShowAlert = jest.fn(); - const setup = ( - setIsShowAlert: (value: boolean) => void, - isShowAlert: boolean, - isVerifyTimeOut: boolean, - moduleType: string, - ) => { - return render( - , - ); + const onCloseSpy = jest.fn(); + const setup = (onClose: () => void, showAlert: boolean, moduleType: string) => { + return render(); }; it('should render board message given moduleType is board', () => { - setup(setIsShowAlert, true, true, 'Board'); + setup(onCloseSpy, true, 'Board'); const message = screen.getByText('Board'); expect(message).toBeInTheDocument(); }); - it('should not render the alert given isVerifyTimeOut or isShowAlert is false', () => { - setup(setIsShowAlert, false, true, 'Board'); - expect(screen.queryByText('Board')).not.toBeInTheDocument(); - - setup(setIsShowAlert, true, false, 'Board'); - - expect(screen.queryByText('Board')).not.toBeInTheDocument(); - }); - - it('should call setIsShowAlert with false when click the close icon given init value', async () => { - setup(setIsShowAlert, true, true, 'any'); + it('should call onCloseSpy when click the close icon given init value', async () => { + setup(onCloseSpy, true, 'any'); const closeIcon = screen.getByTestId('CloseIcon'); act(() => { @@ -46,8 +25,7 @@ describe('TimeoutAlert', () => { }); await waitFor(() => { - expect(setIsShowAlert).toHaveBeenCalledTimes(1); - expect(setIsShowAlert).toHaveBeenCalledWith(false); + expect(onCloseSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx b/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx index 502300bdf0..1588c7214f 100644 --- a/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx +++ b/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx @@ -8,9 +8,13 @@ import { PROJECT_NAME_LABEL, SAVE, STEPPER, + VERIFY, TEST_PROJECT_NAME, VELOCITY, COMMON_TIME_FORMAT, + REQUIRED_DATA, + MOCK_PIPELINE_VERIFY_URL, + MOCK_BOARD_URL_FOR_JIRA, } from '../../fixtures'; import { updateCycleTimeSettings, @@ -20,14 +24,9 @@ import { updateDeploymentFrequencySettings, updateTreatFlagCardAsBlock, } from '@src/context/Metrics/metricsSlice'; -import { - updateBoardVerifyState, - updateMetrics, - updatePipelineToolVerifyState, - updateSourceControlVerifyState, -} from '@src/context/config/configSlice'; -import { act, render, screen, waitFor } from '@testing-library/react'; +import { act, render, screen, waitFor, within } from '@testing-library/react'; import { ASSIGNEE_FILTER_TYPES } from '@src/constants/resources'; +import { updateMetrics } from '@src/context/config/configSlice'; import MetricsStepper from '@src/containers/MetricsStepper'; import { setupStore } from '../../utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; @@ -40,7 +39,7 @@ import { rest } from 'msw'; import dayjs from 'dayjs'; import React from 'react'; -const START_DATE_LABEL = 'From *'; +const START_DATE_LABEL = 'From'; const TODAY = dayjs(); const INPUT_DATE_VALUE = TODAY.format('MM/DD/YYYY'); const YES = 'Yes'; @@ -98,23 +97,41 @@ jest.mock('@src/hooks/useGenerateReportEffect', () => ({ }), })); -const server = setupServer(rest.post(MOCK_REPORT_URL, (_, res, ctx) => res(ctx.status(HttpStatusCode.Ok)))); +const server = setupServer( + rest.post(MOCK_REPORT_URL, (_, res, ctx) => res(ctx.status(HttpStatusCode.Ok))), + rest.post(MOCK_BOARD_URL_FOR_JIRA, (_, res, ctx) => res(ctx.status(HttpStatusCode.NoContent))), +); -const mockLocation = { reload: jest.fn() }; +const mockLocation = { ...window.location, reload: jest.fn() }; Object.defineProperty(window, 'location', { value: mockLocation }); let store = setupStore(); -const fillConfigPageData = async () => { +const fillAndVerifyConfigPageData = async () => { const projectNameInput = await screen.findByRole('textbox', { name: PROJECT_NAME_LABEL }); await userEvent.type(projectNameInput, TEST_PROJECT_NAME); const startDateInput = (await screen.findByRole('textbox', { name: START_DATE_LABEL })) as HTMLInputElement; await userEvent.type(startDateInput, INPUT_DATE_VALUE); - - act(() => { - store.dispatch(updateMetrics([VELOCITY])); - store.dispatch(updateBoardVerifyState(true)); - store.dispatch(updatePipelineToolVerifyState(true)); - store.dispatch(updateSourceControlVerifyState(true)); + await userEvent.click(screen.getByRole('combobox', { name: REQUIRED_DATA })); + const requireMetricsSelection = within(screen.getByRole('listbox')); + await userEvent.click(requireMetricsSelection.getByRole('option', { name: VELOCITY })); + await userEvent.keyboard('{Escape}'); + const boardConfigModule = screen.getByLabelText('Board Config'); + + expect(boardConfigModule).toBeInTheDocument(); + + const boardIdInput = within(boardConfigModule).getByRole('textbox', { name: 'Board Id' }); + await userEvent.type(boardIdInput, '2'); + const emailInput = within(boardConfigModule).getByRole('textbox', { name: 'Email' }); + await userEvent.type(emailInput, 'user@test.com'); + const siteInput = within(boardConfigModule).getByRole('textbox', { name: 'Site' }); + await userEvent.type(siteInput, 'dorametrics'); + const tokenInput = within(boardConfigModule).getByLabelText('Token *'); + await userEvent.type(tokenInput, 'mockJiraToken'); + const verifyBoardButton = within(boardConfigModule).getByText(VERIFY); + await userEvent.click(verifyBoardButton); + + await waitFor(() => { + expect(screen.getByText(NEXT)).toBeEnabled(); }); }; @@ -125,20 +142,20 @@ const fillMetricsData = () => { }; const fillMetricsPageDate = async () => { - act(() => { + await act(async () => { store.dispatch(saveTargetFields([{ name: 'mockClassification', key: 'mockClassification', flag: true }])); store.dispatch(saveUsers(['mockUsers'])); - store.dispatch(saveDoneColumn(['Done', 'Canceled'])), - store.dispatch( - updateCycleTimeSettings([ - { column: 'Testing', status: 'testing', value: 'Done' }, - { column: 'Testing', status: 'test', value: 'Done' }, - ]), - ); - store.dispatch(updateTreatFlagCardAsBlock(false)), - store.dispatch( - updateDeploymentFrequencySettings({ updateId: 0, label: 'organization', value: 'mock new organization' }), - ); + store.dispatch(saveDoneColumn(['Done', 'Canceled'])); + store.dispatch( + updateCycleTimeSettings([ + { column: 'Testing', status: 'testing', value: 'Done' }, + { column: 'Testing', status: 'test', value: 'Done' }, + ]), + ); + store.dispatch(updateTreatFlagCardAsBlock(false)); + store.dispatch( + updateDeploymentFrequencySettings({ updateId: 0, label: 'organization', value: 'mock new organization' }), + ); store.dispatch( updateDeploymentFrequencySettings({ updateId: 0, label: 'pipelineName', value: 'mock new pipelineName' }), ); @@ -146,16 +163,17 @@ const fillMetricsPageDate = async () => { }); }; -describe('MetricsStepper', () => { - beforeAll(() => server.listen()); - afterAll(() => server.close()); - beforeEach(() => { - store = setupStore(); - }); - afterEach(() => { - navigateMock.mockClear(); - }); +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); +afterAll(() => server.close()); +beforeEach(() => { + store = setupStore(); +}); +afterEach(() => { + server.resetHandlers(); + navigateMock.mockClear(); +}); +describe('MetricsStepper', () => { const setup = () => render( @@ -221,34 +239,38 @@ describe('MetricsStepper', () => { }); it('should enable next when every selected component is show and verified', async () => { - setup(); - await fillConfigPageData(); - - expect(screen.getByText(NEXT)).toBeEnabled(); + server.use(rest.post(MOCK_PIPELINE_VERIFY_URL, (_, res, ctx) => res(ctx.status(HttpStatusCode.NoContent)))); + await act(async () => { + setup(); + }); + await fillAndVerifyConfigPageData(); }); it('should disable next when board component is exist but not verified successfully', async () => { setup(); act(() => { store.dispatch(updateMetrics([VELOCITY])); - store.dispatch(updateBoardVerifyState(false)); }); expect(screen.getByText(NEXT)).toBeDisabled(); }); it('should go metrics page when click next button given next button enabled', async () => { - setup(); + await act(async () => { + setup(); + }); - await fillConfigPageData(); + await fillAndVerifyConfigPageData(); await userEvent.click(screen.getByText(NEXT)); - expect(screen.getByText(METRICS)).toHaveStyle(`color:${stepperColor}`); + expect(screen.getByText(METRICS)).toHaveClass('Mui-active'); }); it('should show metrics export step when click next button given export step', async () => { - setup(); - await fillConfigPageData(); + await act(async () => { + setup(); + }); + await fillAndVerifyConfigPageData(); await userEvent.click(screen.getByText(NEXT)); await fillMetricsPageDate(); waitFor(() => { @@ -274,6 +296,7 @@ describe('MetricsStepper', () => { startDate: null, }, ], + sortType: 'DEFAULT', metrics: [], pipelineTool: undefined, projectName: '', @@ -297,6 +320,7 @@ describe('MetricsStepper', () => { startDate: null, }, ], + sortType: 'DEFAULT', metrics: ['Velocity'], pipelineTool: undefined, projectName: '', @@ -316,7 +340,6 @@ describe('MetricsStepper', () => { const expectedJson = { advancedSettings: null, assigneeFilter: ASSIGNEE_FILTER_TYPES.LAST_ASSIGNEE, - board: { boardId: '', email: '', site: '', token: '', type: 'Jira' }, calendarType: 'Regular Calendar(Weekend Considered)', dateRange: [ { @@ -325,8 +348,16 @@ describe('MetricsStepper', () => { }, ], metrics: ['Velocity'], - pipelineCrews: undefined, + board: { + type: 'Jira', + boardId: '', + email: '', + site: '', + token: '', + }, pipelineTool: undefined, + sortType: 'DEFAULT', + pipelineCrews: undefined, projectName: 'test project Name', sourceControl: undefined, classification: undefined, @@ -337,13 +368,23 @@ describe('MetricsStepper', () => { leadTime: undefined, reworkTimesSettings: DEFAULT_REWORK_SETTINGS, }; - setup(); + await act(() => { + setup(); + }); - await fillConfigPageData(); + await fillAndVerifyConfigPageData(); await userEvent.click(screen.getByText(NEXT)); + const saveButton = screen.getByText(SAVE); + expect(screen.getByText(METRICS)).toHaveClass('Mui-active'); + + waitFor(() => { + expect(saveButton).toBeInTheDocument(); + }); await userEvent.click(screen.getByText(SAVE)); - expect(exportToJsonFile).toHaveBeenCalledWith(expectedFileName, expectedJson); + await waitFor(() => { + expect(exportToJsonFile).toHaveBeenCalledWith(expectedFileName, expectedJson); + }); }, 50000); it('should export json file when click save button in report page given all content is empty', async () => { @@ -366,6 +407,7 @@ describe('MetricsStepper', () => { sourceControl: undefined, classification: ['mockClassification'], crews: ['mockUsers'], + sortType: 'DEFAULT', cycleTime: { jiraColumns: [ { @@ -384,13 +426,20 @@ describe('MetricsStepper', () => { }, }; - setup(); - await fillConfigPageData(); + await act(() => { + setup(); + }); + await fillAndVerifyConfigPageData(); await userEvent.click(screen.getByText(NEXT)); + + expect(screen.getByText(METRICS)).toHaveClass('Mui-active'); + await fillMetricsPageDate(); + waitFor(() => { expect(screen.getByText(NEXT)).toBeInTheDocument(); }); + await userEvent.click(screen.getByText(NEXT)); await waitFor(() => { diff --git a/frontend/__tests__/context/boardSlice.test.ts b/frontend/__tests__/context/boardSlice.test.ts index 636268bd06..916ccb5cfe 100644 --- a/frontend/__tests__/context/boardSlice.test.ts +++ b/frontend/__tests__/context/boardSlice.test.ts @@ -1,24 +1,8 @@ -import boardReducer, { - updateBoard, - updateBoardVerifyState, - updateJiraVerifyResponse, -} from '@src/context/config/configSlice'; +import boardReducer, { updateBoard, updateJiraVerifyResponse } from '@src/context/config/configSlice'; import { MOCK_JIRA_VERIFY_RESPONSE } from '../fixtures'; import initialConfigState from '../initialConfigState'; describe('board reducer', () => { - it('should return false when handle initial state', () => { - const result = boardReducer(undefined, { type: 'unknown' }); - - expect(result.board.isVerified).toEqual(false); - }); - - it('should return true when handle changeBoardVerifyState given isBoardVerified is true', () => { - const result = boardReducer(initialConfigState, updateBoardVerifyState(true)); - - expect(result.board.isVerified).toEqual(true); - }); - it('should update board fields when change board fields input', () => { const board = boardReducer(initialConfigState, updateBoard({ boardId: '1' })); diff --git a/frontend/__tests__/context/configSlice.test.ts b/frontend/__tests__/context/configSlice.test.ts index 9481daff1c..bc6d4f85ac 100644 --- a/frontend/__tests__/context/configSlice.test.ts +++ b/frontend/__tests__/context/configSlice.test.ts @@ -9,7 +9,7 @@ import configReducer, { updateProjectName, } from '@src/context/config/configSlice'; import { CHINA_CALENDAR, CONFIG_PAGE_VERIFY_IMPORT_ERROR_MESSAGE, REGULAR_CALENDAR, VELOCITY } from '../fixtures'; -import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { setupStore } from '@test/utils/setupStoreUtil'; import initialConfigState from '../initialConfigState'; diff --git a/frontend/__tests__/context/pipelineToolSlice.test.ts b/frontend/__tests__/context/pipelineToolSlice.test.ts index 33a164732c..0044d28786 100644 --- a/frontend/__tests__/context/pipelineToolSlice.test.ts +++ b/frontend/__tests__/context/pipelineToolSlice.test.ts @@ -7,7 +7,6 @@ import { updatePipelineTool, updatePipelineToolVerifyResponse, updatePipelineToolVerifyResponseSteps, - updatePipelineToolVerifyState, } from '@src/context/config/configSlice'; import { MOCK_BUILD_KITE_VERIFY_RESPONSE, PIPELINE_TOOL_TYPES } from '../fixtures'; import configReducer from '@src/context/config/configSlice'; @@ -60,18 +59,6 @@ describe('pipelineTool reducer', () => { }, ]; - it('should set isPipelineToolVerified false when handle initial state', () => { - const result = configReducer(undefined, { type: 'unknown' }); - - expect(result.pipelineTool.isVerified).toEqual(false); - }); - - it('should set isPipelineToolVerified true when handle updatePipelineToolVerifyState given isPipelineToolVerified is true', () => { - const result = configReducer(initialConfigState, updatePipelineToolVerifyState(true)); - - expect(result.pipelineTool.isVerified).toEqual(true); - }); - it('should update pipelineTool fields when change pipelineTool fields input', () => { const config = configReducer(initialConfigState, updatePipelineTool({ token: 'abcd' })); @@ -86,7 +73,6 @@ describe('pipelineTool reducer', () => { type: PIPELINE_TOOL_TYPES.BUILD_KITE, token: '', }, - isVerified: false, isShow: false, verifiedResponse: { pipelineList: [ @@ -134,7 +120,6 @@ describe('pipelineTool reducer', () => { type: PIPELINE_TOOL_TYPES.BUILD_KITE, token: '', }, - isVerified: false, isShow: false, verifiedResponse: { pipelineList: [ diff --git a/frontend/__tests__/context/sourceControlSlice.test.ts b/frontend/__tests__/context/sourceControlSlice.test.ts index 6c46a1289a..f8f0c1f645 100644 --- a/frontend/__tests__/context/sourceControlSlice.test.ts +++ b/frontend/__tests__/context/sourceControlSlice.test.ts @@ -1,24 +1,11 @@ import sourceControlReducer, { updateSourceControl, updateSourceControlVerifiedResponse, - updateSourceControlVerifyState, } from '@src/context/config/configSlice'; import { MOCK_GITHUB_VERIFY_RESPONSE } from '../fixtures'; import initialConfigState from '../initialConfigState'; describe('sourceControl reducer', () => { - it('should set isSourceControlVerified false when handle initial state', () => { - const sourceControl = sourceControlReducer(undefined, { type: 'unknown' }); - - expect(sourceControl.sourceControl.isVerified).toEqual(false); - }); - - it('should return true when handle changeSourceControlVerifyState given isSourceControlVerified is true', () => { - const sourceControl = sourceControlReducer(initialConfigState, updateSourceControlVerifyState(true)); - - expect(sourceControl.sourceControl.isVerified).toEqual(true); - }); - it('should update sourceControl fields when change sourceControl fields input', () => { const sourceControl = sourceControlReducer(initialConfigState, updateSourceControl({ token: 'token' })); diff --git a/frontend/__tests__/fixtures.ts b/frontend/__tests__/fixtures.ts index 2e09481238..47d570ae23 100644 --- a/frontend/__tests__/fixtures.ts +++ b/frontend/__tests__/fixtures.ts @@ -1,5 +1,5 @@ -import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; import { CSVReportRequestDTO, ReportRequestDTO } from '@src/clients/report/dto/request'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; import { SOURCE_CONTROL_TYPES } from '@src/constants/resources'; import { IStepsParams } from '@src/clients/MetricsClient'; @@ -651,6 +651,7 @@ export const MOCK_SOURCE_CONTROL_VERIFY_ERROR_CASE_TEXT = 'Token is incorrect!'; export const MOCK_PIPELINE_VERIFY_UNAUTHORIZED_TEXT = 'Token is incorrect!'; export const MOCK_PIPELINE_VERIFY_FORBIDDEN_ERROR_TEXT = 'Forbidden request, please change your token with correct access permission.'; +export const UNKNOWN_ERROR_TEXT = 'Unknown error'; export const FAKE_TOKEN = 'fake-token'; @@ -694,3 +695,7 @@ export const TIME_RANGE_ERROR_MESSAGE = { }; export const COMMON_TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; + +export const PIPELINE_TOOL_TOKEN_INPUT_LABEL = 'input token'; + +export const TIMEOUT_ALERT_TEST_ID = 'timeoutAlert'; diff --git a/frontend/__tests__/hooks/useVerifyBoardEffect.test.tsx b/frontend/__tests__/hooks/useVerifyBoardEffect.test.tsx index 9a781cd93f..e45e580c72 100644 --- a/frontend/__tests__/hooks/useVerifyBoardEffect.test.tsx +++ b/frontend/__tests__/hooks/useVerifyBoardEffect.test.tsx @@ -1,161 +1,154 @@ -import { useVerifyBoardEffect, useVerifyBoardStateInterface } from '@src/hooks/useVerifyBoardEffect'; -import { act, renderHook, waitFor } from '@testing-library/react'; -import { FAKE_TOKEN } from '@test/fixtures'; -import { HttpStatusCode } from 'axios'; - +import { boardConfigDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; +import { boardConfigSchema } from '@src/containers/ConfigStep/Form/schema'; +import { useVerifyBoardEffect } from '@src/hooks/useVerifyBoardEffect'; import { InternalServerError } from '@src/errors/InternalServerError'; import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources'; import { UnauthorizedError } from '@src/errors/UnauthorizedError'; import { boardClient } from '@src/clients/board/BoardClient'; import { NotFoundError } from '@src/errors/NotFoundError'; import { TimeoutError } from '@src/errors/TimeoutError'; -import { BOARD_TYPES } from '@test/fixtures'; - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useDispatch: () => mockDispatch, -})); - -jest.mock('@src/hooks/useAppDispatch', () => ({ - useAppSelector: () => ({ type: BOARD_TYPES.JIRA }), - useAppDispatch: jest.fn(() => jest.fn()), -})); - -const updateFields = (result: { current: useVerifyBoardStateInterface }) => { - result.current.updateField('Board Id', '1'); - result.current.updateField('Email', 'fake@qq.com'); - result.current.updateField('Site', 'fake'); - result.current.updateField('Token', FAKE_TOKEN); +import { FormProvider } from '@test/utils/FormProvider'; +import { setupStore } from '../utils/setupStoreUtil'; +import { renderHook } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { HttpStatusCode } from 'axios'; +import { ReactNode } from 'react'; + +const setErrorSpy = jest.fn(); +const resetSpy = jest.fn(); + +jest.mock('react-hook-form', () => { + return { + ...jest.requireActual('react-hook-form'), + useFormContext: () => { + const { useFormContext } = jest.requireActual('react-hook-form'); + const originals = useFormContext(); + return { + ...originals, + setError: (...args: [string, { message: string }]) => setErrorSpy(...args), + reset: (...args: [string, { message: string }]) => resetSpy(...args), + }; + }, + }; +}); + +const HookWrapper = ({ children }: { children: ReactNode }) => { + const store = setupStore(); + return ( + + + {children} + + + ); +}; + +const setup = () => { + const { result } = renderHook(useVerifyBoardEffect, { wrapper: HookWrapper }); + + return { result }; }; describe('use verify board state', () => { + beforeEach(() => { + setErrorSpy.mockClear(); + resetSpy.mockClear(); + }); afterAll(() => { jest.clearAllMocks(); }); it('should got initial data state when hook render given none input', async () => { - const { result } = renderHook(() => useVerifyBoardEffect()); + const { result } = setup(); expect(result.current.isLoading).toBe(false); expect(result.current.fields.length).toBe(5); }); - it('should got email and token fields error message when call verify function given a invalid token', async () => { - const mockedError = new UnauthorizedError('', HttpStatusCode.Unauthorized, ''); - boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(mockedError)); - - const { result } = renderHook(() => useVerifyBoardEffect()); - await act(async () => { - await updateFields(result); - await result.current.verifyJira(); - }); - - const emailFiled = result.current.fields.find((field) => field.key === 'Email'); - const tokenField = result.current.fields.find((field) => field.key === 'Token'); - expect(emailFiled?.verifiedError).toBe('Email is incorrect!'); - expect(tokenField?.verifiedError).toBe( - 'Token is invalid, please change your token with correct access permission!', + it('should keep verified values when call verify function given a valid token', async () => { + const mockedOkResponse = { + response: 'ok', + }; + boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.resolve(mockedOkResponse)); + + const { result } = setup(); + await result.current.verifyJira(); + + expect(resetSpy).toHaveBeenCalledWith( + { + type: 'Jira', + boardId: '', + email: '', + site: '', + token: '', + }, + { keepValues: true }, ); }); - it('should clear email validatedError when updateField by Email given fetch error ', async () => { + it('should got email and token fields error message when call verify function given a invalid token', async () => { const mockedError = new UnauthorizedError('', HttpStatusCode.Unauthorized, ''); boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(mockedError)); - const { result } = renderHook(() => useVerifyBoardEffect()); - await act(async () => { - await updateFields(result); - await result.current.verifyJira(); - }); + const { result } = setup(); + await result.current.verifyJira(); - const emailFiled = result.current.fields.find((field) => field.key === 'Email'); - expect(emailFiled?.verifiedError).toBe('Email is incorrect!'); - - await act(async () => { - await result.current.updateField('Email', 'fake@qq.com'); + expect(setErrorSpy).toHaveBeenCalledWith('email', { message: 'Email is incorrect!' }); + expect(setErrorSpy).toHaveBeenCalledWith('token', { + message: 'Token is invalid, please change your token with correct access permission!', }); - const emailText = result.current.fields.find((field) => field.key === 'Email'); - expect(emailText?.verifiedError).toBe(''); }); it('should got site field error message when call verify function given a invalid site', async () => { const mockedError = new NotFoundError('site is incorrect', HttpStatusCode.NotFound, 'site is incorrect'); boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(mockedError)); - const { result } = renderHook(() => useVerifyBoardEffect()); - await act(async () => { - await updateFields(result); - await result.current.verifyJira(); - }); - - await waitFor(() => { - const site = result.current.fields.find((field) => field.key === 'Site'); + const { result } = setup(); + await result.current.verifyJira(); - expect(site?.verifiedError).toBe('Site is incorrect!'); - }); + expect(setErrorSpy).toHaveBeenCalledWith('site', { message: 'Site is incorrect!' }); }); it('should got board id field error message when call verify function given a invalid board id', async () => { const mockedError = new NotFoundError('boardId is incorrect', HttpStatusCode.NotFound, 'boardId is incorrect'); boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(mockedError)); - const { result } = renderHook(() => useVerifyBoardEffect()); - await act(() => { - updateFields(result); - result.current.verifyJira(); - }); + const { result } = setup(); + await result.current.verifyJira(); - await waitFor(() => { - const boardId = result.current.fields.find((field) => field.key === 'Board Id'); - expect(boardId?.verifiedError).toBe('Board Id is incorrect!'); - }); + expect(setErrorSpy).toHaveBeenCalledWith('boardId', { message: 'Board Id is incorrect!' }); }); it('should got token fields error message when call verify function given a unknown error', async () => { const mockedError = new InternalServerError('', HttpStatusCode.ServiceUnavailable, ''); boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(mockedError)); - const { result } = renderHook(() => useVerifyBoardEffect()); - await act(async () => { - await updateFields(result); - await result.current.verifyJira(); - }); + const { result } = setup(); + await result.current.verifyJira(); - const tokenField = result.current.fields.find((field) => field.key === 'Token'); - expect(tokenField?.verifiedError).toBe('Unknown error'); + expect(setErrorSpy).toHaveBeenCalledWith('token', { message: 'Unknown error' }); }); - it('should clear all verified error messages when update a verified error field', async () => { - const mockedError = new UnauthorizedError('', HttpStatusCode.Unauthorized, ''); + it('should set timeout is true given getVerifyBoard api is timeout', async () => { + const mockedError = new TimeoutError('', AXIOS_REQUEST_ERROR_CODE.TIMEOUT); boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(mockedError)); - const { result } = renderHook(() => useVerifyBoardEffect()); - await act(() => { - updateFields(result); - result.current.verifyJira(); - }); - await waitFor(() => { - result.current.updateField('Token', 'fake-token-new'); - }); + const { result } = setup(); + await result.current.verifyJira(); - const emailFiled = result.current.fields.find((field) => field.key === 'Email'); - const tokenField = result.current.fields.find((field) => field.key === 'Token'); - expect(emailFiled?.verifiedError).toBe(''); - expect(tokenField?.verifiedError).toBe(''); + expect(setErrorSpy).toHaveBeenCalledWith('token', { message: 'Timeout!' }); }); - it('should set timeout is true given getVerifyBoard api is timeout', async () => { - const mockedError = new TimeoutError('', AXIOS_REQUEST_ERROR_CODE.TIMEOUT); - boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(mockedError)); + it('should clear all verified error messages when call resetFeilds', async () => { + const { result } = setup(); - const { result } = renderHook(() => useVerifyBoardEffect()); - await act(() => { - result.current.verifyJira(); - }); + result.current.resetFields(); - await waitFor(() => { - const isVerifyTimeOut = result.current.isVerifyTimeOut; - expect(isVerifyTimeOut).toBe(true); + expect(resetSpy).toHaveBeenCalledWith({ + type: 'Jira', + boardId: '', + email: '', + site: '', + token: '', }); }); }); diff --git a/frontend/__tests__/hooks/useVerifyPipelineToolEffect.test.tsx b/frontend/__tests__/hooks/useVerifyPipelineToolEffect.test.tsx index f8e8bd4da1..d379a3a39b 100644 --- a/frontend/__tests__/hooks/useVerifyPipelineToolEffect.test.tsx +++ b/frontend/__tests__/hooks/useVerifyPipelineToolEffect.test.tsx @@ -1,87 +1,136 @@ import { MOCK_PIPELINE_VERIFY_FORBIDDEN_ERROR_TEXT, - MOCK_PIPELINE_VERIFY_REQUEST_PARAMS, MOCK_PIPELINE_VERIFY_UNAUTHORIZED_TEXT, + UNKNOWN_ERROR_TEXT, } from '../fixtures'; +import { pipelineToolDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; import { useVerifyPipelineToolEffect } from '@src/hooks/useVerifyPipelineToolEffect'; import { pipelineToolClient } from '@src/clients/pipeline/PipelineToolClient'; +import { pipelineToolSchema } from '@src/containers/ConfigStep/Form/schema'; import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources'; -import { act, renderHook, waitFor } from '@testing-library/react'; +import { FormProvider } from '@test/utils/FormProvider'; +import { setupStore } from '../utils/setupStoreUtil'; +import { renderHook } from '@testing-library/react'; +import { Provider } from 'react-redux'; import { HttpStatusCode } from 'axios'; +import { ReactNode } from 'react'; + +const setErrorSpy = jest.fn(); +const resetSpy = jest.fn(); + +jest.mock('react-hook-form', () => { + return { + ...jest.requireActual('react-hook-form'), + useFormContext: () => { + const { useFormContext } = jest.requireActual('react-hook-form'); + const originals = useFormContext(); + return { + ...originals, + setError: (...args: [string, { message: string }]) => { + setErrorSpy(...args); + }, + reset: (...args: [string, { message: string }]) => resetSpy(...args), + }; + }, + }; +}); -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useDispatch: () => mockDispatch, -})); +const HookWrapper = ({ children }: { children: ReactNode }) => { + const store = setupStore(); -describe('use verify pipelineTool state', () => { - it('should return empty error message when call verify feature given client returns 204', async () => { - pipelineToolClient.verify = jest.fn().mockResolvedValue({ - code: HttpStatusCode.NoContent, - }); + return ( + + + {children} + + + ); +}; - const { result } = renderHook(() => useVerifyPipelineToolEffect()); +const setup = () => { + const { result } = renderHook(useVerifyPipelineToolEffect, { wrapper: HookWrapper }); - act(() => { - result.current.verifyPipelineTool(MOCK_PIPELINE_VERIFY_REQUEST_PARAMS); - }); + return { result }; +}; - await waitFor(() => { - expect(result.current.verifiedError).toEqual(''); - expect(result.current.isLoading).toEqual(false); - }); +describe('use verify pipelineTool state', () => { + beforeEach(() => { + setErrorSpy.mockClear(); + resetSpy.mockClear(); }); - - it('should set error message when verifying pipeline given response status 401', async () => { - pipelineToolClient.verify = jest.fn().mockResolvedValue({ - code: HttpStatusCode.Unauthorized, - errorTitle: MOCK_PIPELINE_VERIFY_UNAUTHORIZED_TEXT, - }); - - const { result } = renderHook(() => useVerifyPipelineToolEffect()); - - act(() => { - result.current.verifyPipelineTool(MOCK_PIPELINE_VERIFY_REQUEST_PARAMS); - }); - - await waitFor(() => { - expect(result.current.verifiedError).toEqual(MOCK_PIPELINE_VERIFY_UNAUTHORIZED_TEXT); - }); + afterAll(() => { + jest.clearAllMocks(); }); - - it('should clear error message when explicitly call clear function given error message exists', async () => { - pipelineToolClient.verify = jest - .fn() - .mockResolvedValue({ code: HttpStatusCode.Forbidden, errorTitle: MOCK_PIPELINE_VERIFY_FORBIDDEN_ERROR_TEXT }); - const { result } = renderHook(() => useVerifyPipelineToolEffect()); - - act(() => { - result.current.verifyPipelineTool(MOCK_PIPELINE_VERIFY_REQUEST_PARAMS); - }); - - await waitFor(() => { - expect(result.current.verifiedError).toEqual(MOCK_PIPELINE_VERIFY_FORBIDDEN_ERROR_TEXT); + it('should keep verified values when call verify feature given client returns 204', async () => { + pipelineToolClient.verify = jest.fn().mockResolvedValue({ + code: HttpStatusCode.NoContent, }); - result.current.clearVerifiedError(); + const { result } = setup(); + await result.current.verifyPipelineTool(); - await waitFor(() => { - expect(result.current.verifiedError).toEqual(''); - }); + expect(resetSpy).toHaveBeenCalledWith({ type: 'BuildKite', token: '' }, { keepValues: true }); }); - it('should set timeout is true when verify api is timeout', async () => { - pipelineToolClient.verify = jest.fn().mockResolvedValue({ code: AXIOS_REQUEST_ERROR_CODE.TIMEOUT }); - - const { result } = renderHook(() => useVerifyPipelineToolEffect()); - await act(() => { - result.current.verifyPipelineTool(MOCK_PIPELINE_VERIFY_REQUEST_PARAMS); - }); - - await waitFor(() => { - const isVerifyTimeOut = result.current.isVerifyTimeOut; - expect(isVerifyTimeOut).toBe(true); + const errorScenarios = [ + { + mock: { + code: HttpStatusCode.Unauthorized, + errorTitle: MOCK_PIPELINE_VERIFY_UNAUTHORIZED_TEXT, + }, + field: 'token', + status: '401', + message: 'Token is incorrect!', + }, + { + mock: { + code: HttpStatusCode.Forbidden, + errorTitle: MOCK_PIPELINE_VERIFY_FORBIDDEN_ERROR_TEXT, + }, + field: 'token', + status: '403', + message: 'Forbidden request, please change your token with correct access permission.', + }, + { + mock: { + code: HttpStatusCode.ServiceUnavailable, + errorTitle: UNKNOWN_ERROR_TEXT, + }, + field: 'token', + status: 'Unknown', + message: 'Unknown error', + }, + { + mock: { + code: AXIOS_REQUEST_ERROR_CODE.TIMEOUT, + errorTitle: '', + }, + field: 'token', + status: 'Timeout', + message: 'Timeout!', + }, + ]; + + it.each(errorScenarios)( + 'should set $field error message when verifying pipeline given response status', + async ({ mock, field, message }) => { + pipelineToolClient.verify = jest.fn().mockResolvedValue(mock); + + const { result } = setup(); + await result.current.verifyPipelineTool(); + + expect(setErrorSpy).toHaveBeenCalledWith(field, { message }); + }, + ); + + it('should clear all verified error messages when call resetFeilds', async () => { + const { result } = setup(); + + result.current.resetFields(); + + expect(resetSpy).toHaveBeenCalledWith({ + type: 'BuildKite', + token: '', }); }); }); diff --git a/frontend/__tests__/hooks/useVerifySourceControlTokenEffect.test.tsx b/frontend/__tests__/hooks/useVerifySourceControlTokenEffect.test.tsx index 21951c67e7..4602088f6c 100644 --- a/frontend/__tests__/hooks/useVerifySourceControlTokenEffect.test.tsx +++ b/frontend/__tests__/hooks/useVerifySourceControlTokenEffect.test.tsx @@ -1,94 +1,119 @@ -import { MOCK_SOURCE_CONTROL_VERIFY_ERROR_CASE_TEXT, MOCK_SOURCE_CONTROL_VERIFY_REQUEST_PARAMS } from '../fixtures'; import { useVerifySourceControlTokenEffect } from '@src/hooks/useVerifySourceControlTokenEffect'; +import { sourceControlDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; +import { MOCK_PIPELINE_VERIFY_UNAUTHORIZED_TEXT, UNKNOWN_ERROR_TEXT } from '../fixtures'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; -import { ContextProvider } from '@src/hooks/useMetricsStepValidationCheckContext'; +import { sourceControlSchema } from '@src/containers/ConfigStep/Form/schema'; import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources'; -import { act, renderHook, waitFor } from '@testing-library/react'; +import { FormProvider } from '@test/utils/FormProvider'; import { setupStore } from '../utils/setupStoreUtil'; +import { renderHook } from '@testing-library/react'; import { Provider } from 'react-redux'; import { HttpStatusCode } from 'axios'; -import React from 'react'; +import { ReactNode } from 'react'; + +const setErrorSpy = jest.fn(); +const resetSpy = jest.fn(); + +jest.mock('react-hook-form', () => { + return { + ...jest.requireActual('react-hook-form'), + useFormContext: () => { + const { useFormContext } = jest.requireActual('react-hook-form'); + const originals = useFormContext(); + return { + ...originals, + setError: (...args: [string, { message: string }]) => setErrorSpy(...args), + reset: (...args: [string, { message: string }]) => resetSpy(...args), + }; + }, + }; +}); + +const HookWrapper = ({ children }: { children: ReactNode }) => { + const store = setupStore(); + return ( + + + {children} + + + ); +}; describe('use verify sourceControl token', () => { const setup = () => { - const store = setupStore(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); - const { result } = renderHook(() => useVerifySourceControlTokenEffect(), { wrapper }); + const { result } = renderHook(useVerifySourceControlTokenEffect, { wrapper: HookWrapper }); return { result }; }; - it('should initial data state when render hook', async () => { + it('should keep verified values when call verify function given a valid token', async () => { const { result } = setup(); - - expect(result.current.isLoading).toEqual(false); - }); - - it('should set error message when get verify sourceControl throw error', async () => { sourceControlClient.verifyToken = jest.fn().mockResolvedValue({ code: HttpStatusCode.NoContent, }); - const { result } = setup(); - - act(() => { - result.current.verifyToken(MOCK_SOURCE_CONTROL_VERIFY_REQUEST_PARAMS); - }); - - await waitFor(() => { - expect(result.current.isLoading).toEqual(false); - }); - await waitFor(() => expect(result.current.verifiedError).toBeUndefined()); - }); - it('should set error message when get verify sourceControl response status 401', async () => { - sourceControlClient.verifyToken = jest.fn().mockResolvedValue({ - code: HttpStatusCode.Unauthorized, - errorTitle: MOCK_SOURCE_CONTROL_VERIFY_ERROR_CASE_TEXT, - }); - const { result } = setup(); + await result.current.verifyToken(); - act(() => { - result.current.verifyToken(MOCK_SOURCE_CONTROL_VERIFY_REQUEST_PARAMS); - }); - - await waitFor(() => { - expect(result.current.isLoading).toEqual(false); - }); - await waitFor(() => { - expect(result.current.verifiedError).toEqual(MOCK_SOURCE_CONTROL_VERIFY_ERROR_CASE_TEXT); - }); - }); - - it('should clear error message when call clearErrorMessage', async () => { - sourceControlClient.verifyToken = jest.fn().mockResolvedValue({ - code: HttpStatusCode.Unauthorized, - errorTitle: MOCK_SOURCE_CONTROL_VERIFY_ERROR_CASE_TEXT, - }); - const { result } = setup(); - - await act(() => result.current.verifyToken(MOCK_SOURCE_CONTROL_VERIFY_REQUEST_PARAMS)); - await act(() => result.current.clearVerifiedError()); - - await waitFor(() => { - expect(result.current.verifiedError).toEqual(''); - }); + expect(resetSpy).toHaveBeenCalledWith( + { + type: 'GitHub', + token: '', + }, + { keepValues: true }, + ); }); - it('should isVerifyTimeOut and isShowAlert is true when api timeout', async () => { - sourceControlClient.verifyToken = jest.fn().mockResolvedValue({ - code: AXIOS_REQUEST_ERROR_CODE.TIMEOUT, - }); + const errorScenarios = [ + { + mock: { + code: HttpStatusCode.Unauthorized, + errorTitle: MOCK_PIPELINE_VERIFY_UNAUTHORIZED_TEXT, + }, + field: 'token', + status: '401', + message: 'Token is incorrect!', + }, + { + mock: { + code: HttpStatusCode.ServiceUnavailable, + errorTitle: UNKNOWN_ERROR_TEXT, + }, + field: 'token', + status: 'Unknown', + message: 'Unknown error', + }, + { + mock: { + code: AXIOS_REQUEST_ERROR_CODE.TIMEOUT, + errorTitle: '', + }, + field: 'token', + status: 'Timeout', + message: 'Timeout!', + }, + ]; + + it.each(errorScenarios)( + 'should set $field error message when verifying pipeline given response status', + async ({ mock, field, message }) => { + sourceControlClient.verifyToken = jest.fn().mockResolvedValue(mock); + + const { result } = setup(); + await result.current.verifyToken(); + + expect(setErrorSpy).toHaveBeenCalledWith(field, { message }); + }, + ); + + it('should clear all verified error messages when call resetFeilds', async () => { const { result } = setup(); - await act(() => result.current.verifyToken(MOCK_SOURCE_CONTROL_VERIFY_REQUEST_PARAMS)); + result.current.resetFields(); - await waitFor(() => { - expect(result.current.isVerifyTimeOut).toBeTruthy(); - expect(result.current.isShowAlert).toBeTruthy(); + expect(resetSpy).toHaveBeenCalledWith({ + type: 'GitHub', + token: '', }); }); }); diff --git a/frontend/__tests__/initialConfigState.ts b/frontend/__tests__/initialConfigState.ts index b0500db339..d71e47f4a3 100644 --- a/frontend/__tests__/initialConfigState.ts +++ b/frontend/__tests__/initialConfigState.ts @@ -1,5 +1,5 @@ -import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; import { BOARD_TYPES, PIPELINE_TOOL_TYPES, REGULAR_CALENDAR } from './fixtures'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { BasicConfigState } from '@src/context/config/configSlice'; import { SOURCE_CONTROL_TYPES } from '@src/constants/resources'; @@ -26,7 +26,6 @@ const initialConfigState: BasicConfigState = { site: '', token: '', }, - isVerified: false, isShow: false, verifiedResponse: { jiraColumns: [], @@ -39,7 +38,6 @@ const initialConfigState: BasicConfigState = { type: PIPELINE_TOOL_TYPES.BUILD_KITE, token: '', }, - isVerified: false, isShow: false, verifiedResponse: { pipelineList: [], @@ -50,7 +48,6 @@ const initialConfigState: BasicConfigState = { type: SOURCE_CONTROL_TYPES.GITHUB, token: '', }, - isVerified: false, isShow: false, verifiedResponse: { repoList: [], diff --git a/frontend/__tests__/updatedConfigState.ts b/frontend/__tests__/updatedConfigState.ts index c76d6819ea..e359353db6 100644 --- a/frontend/__tests__/updatedConfigState.ts +++ b/frontend/__tests__/updatedConfigState.ts @@ -21,7 +21,6 @@ const updatedConfigState = { site: '', token: '', }, - isVerified: false, isShow: false, verifiedResponse: { jiraColumns: [], @@ -34,7 +33,6 @@ const updatedConfigState = { type: PIPELINE_TOOL_TYPES.BUILD_KITE, token: '', }, - isVerified: false, isShow: false, verifiedResponse: { pipelineList: [], @@ -45,7 +43,6 @@ const updatedConfigState = { type: SOURCE_CONTROL_TYPES.GITHUB, token: '', }, - isVerified: false, isShow: false, verifiedResponse: { repoList: [], diff --git a/frontend/__tests__/utils/FormProvider.tsx b/frontend/__tests__/utils/FormProvider.tsx new file mode 100644 index 0000000000..e4afe55d4f --- /dev/null +++ b/frontend/__tests__/utils/FormProvider.tsx @@ -0,0 +1,20 @@ +import { useForm, FormProvider as RHFProvider } from 'react-hook-form'; +import { InferType, AnySchema, ObjectSchema } from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { ReactNode } from 'react'; + +interface IFormProviderProps { + children: ReactNode; + defaultValues: InferType; + schema: T; +} + +export const FormProvider = ({ defaultValues, children, schema }: IFormProviderProps>) => { + const formMethods = useForm>({ + defaultValues, + resolver: yupResolver(schema), + mode: 'onChange', + }); + + return {children}; +}; diff --git a/frontend/package.json b/frontend/package.json index 6fc8453091..893667ac41 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^5.0.12", + "@hookform/resolvers": "^3.3.4", "@mui/icons-material": "^5.15.14", "@mui/material": "^5.15.14", "@mui/x-date-pickers": "^7.0.0", @@ -58,11 +59,13 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", + "react-hook-form": "^7.51.3", "react-redux": "^9.0.0", "react-router-dom": "^6.22.3", "typescript": "^5.4.2", "vite": "^5.2.2", - "vite-plugin-pwa": "^0.19.5" + "vite-plugin-pwa": "^0.19.5", + "yup": "^1.4.0" }, "devDependencies": { "@dotenvx/dotenvx": "^0.27.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1546ad761f..9e275e1680 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,22 +10,25 @@ dependencies: version: 11.11.4(@types/react@18.2.67)(react@18.2.0) '@emotion/styled': specifier: ^11.11.0 - version: 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) '@fontsource/roboto': specifier: ^5.0.12 version: 5.0.12 + '@hookform/resolvers': + specifier: ^3.3.4 + version: 3.3.4(react-hook-form@7.51.3) '@mui/icons-material': specifier: ^5.15.14 - version: 5.15.15(@mui/material@5.15.15)(@types/react@18.2.67)(react@18.2.0) + version: 5.15.14(@mui/material@5.15.14)(@types/react@18.2.67)(react@18.2.0) '@mui/material': specifier: ^5.15.14 - version: 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) '@mui/x-date-pickers': specifier: ^7.0.0 - version: 7.2.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.15)(@types/react@18.2.67)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) + version: 7.0.0(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(@types/react@18.2.67)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) '@reduxjs/toolkit': specifier: ^2.2.2 - version: 2.2.3(react-redux@9.1.0)(react@18.2.0) + version: 2.2.2(react-redux@9.1.0)(react@18.2.0) axios: specifier: ^1.6.8 version: 1.6.8 @@ -47,6 +50,9 @@ dependencies: react-error-boundary: specifier: ^4.0.13 version: 4.0.13(react@18.2.0) + react-hook-form: + specifier: ^7.51.3 + version: 7.51.3(react@18.2.0) react-redux: specifier: ^9.0.0 version: 9.1.0(@types/react@18.2.67)(react@18.2.0)(redux@5.0.1) @@ -55,13 +61,16 @@ dependencies: version: 6.22.3(react-dom@18.2.0)(react@18.2.0) typescript: specifier: ^5.4.2 - version: 5.4.5 + version: 5.4.2 vite: specifier: ^5.2.2 - version: 5.2.8(@types/node@20.11.30) + version: 5.2.6(@types/node@20.11.30) vite-plugin-pwa: specifier: ^0.19.5 - version: 0.19.5(vite@5.2.8)(workbox-build@7.0.0)(workbox-window@7.0.0) + version: 0.19.5(vite@5.2.6)(workbox-build@7.0.0)(workbox-window@7.0.0) + yup: + specifier: ^1.4.0 + version: 1.4.0 devDependencies: '@dotenvx/dotenvx': @@ -78,7 +87,7 @@ devDependencies: version: 14.2.2(react-dom@18.2.0)(react@18.2.0) '@testing-library/user-event': specifier: ^14.5.2 - version: 14.5.2(@testing-library/dom@10.0.0) + version: 14.5.2(@testing-library/dom@9.3.4) '@types/jest': specifier: ^29.5.12 version: 29.5.12 @@ -105,13 +114,13 @@ devDependencies: version: 7.1.33 '@typescript-eslint/eslint-plugin': specifier: ^7.3.1 - version: 7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.5) + version: 7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': specifier: ^7.3.1 - version: 7.4.0(eslint@8.57.0)(typescript@5.4.5) + version: 7.4.0(eslint@8.57.0)(typescript@5.4.2) '@vitejs/plugin-react-swc': specifier: ^3.6.0 - version: 3.6.0(vite@5.2.8) + version: 3.6.0(vite@5.2.6) audit-ci: specifier: ^6.6.1 version: 6.6.1 @@ -162,13 +171,13 @@ devDependencies: version: 29.7.0 license-compliance: specifier: ^3.0.0 - version: 3.0.0(typescript@5.4.5) + version: 3.0.0(typescript@5.4.2) lint-staged: specifier: ^15.2.2 version: 15.2.2 msw: specifier: ^1.3.3 - version: 1.3.3(typescript@5.4.5) + version: 1.3.3(typescript@5.4.2) node-fetch: specifier: ^3.3.2 version: 3.3.2 @@ -177,16 +186,16 @@ devDependencies: version: 3.2.5 prettier-plugin-sort-imports: specifier: ^1.8.4 - version: 1.8.5(typescript@5.4.5) + version: 1.8.4(typescript@5.4.2) ts-jest: specifier: ^29.1.2 - version: 29.1.2(@babel/core@7.24.3)(jest@29.7.0)(typescript@5.4.5) + version: 29.1.2(@babel/core@7.24.3)(jest@29.7.0)(typescript@5.4.2) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.11.30)(typescript@5.4.5) + version: 10.9.2(@types/node@20.11.30)(typescript@5.4.2) tsc-files: specifier: ^1.1.4 - version: 1.1.4(typescript@5.4.5) + version: 1.1.4(typescript@5.4.2) packages: @@ -1547,6 +1556,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 + dev: false /@babel/template@7.24.0: resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} @@ -1714,22 +1724,12 @@ packages: csstype: 3.1.3 dev: false - /@emotion/serialize@1.1.4: - resolution: {integrity: sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==} - dependencies: - '@emotion/hash': 0.9.1 - '@emotion/memoize': 0.8.1 - '@emotion/unitless': 0.8.1 - '@emotion/utils': 1.2.1 - csstype: 3.1.3 - dev: false - /@emotion/sheet@1.2.2: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: false - /@emotion/styled@11.11.5(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0): - resolution: {integrity: sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==} + /@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0): + resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 '@types/react': '*' @@ -1738,11 +1738,11 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.2 '@emotion/react': 11.11.4(@types/react@18.2.67)(react@18.2.0) - '@emotion/serialize': 1.1.4 + '@emotion/serialize': 1.1.3 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 '@types/react': 18.2.67 @@ -2037,6 +2037,14 @@ packages: '@hapi/hoek': 9.3.0 dev: true + /@hookform/resolvers@3.3.4(react-hook-form@7.51.3): + resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.51.3(react@18.2.0) + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -2492,7 +2500,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.67) '@mui/utils': 5.15.14(@types/react@18.2.67)(react@18.2.0) @@ -2504,12 +2512,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@mui/core-downloads-tracker@5.15.15: - resolution: {integrity: sha512-aXnw29OWQ6I5A47iuWEI6qSSUfH6G/aCsW9KmW3LiFqr7uXZBK4Ks+z8G+qeIub8k0T5CMqlT2q0L+ZJTMrqpg==} + /@mui/core-downloads-tracker@5.15.14: + resolution: {integrity: sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==} dev: false - /@mui/icons-material@5.15.15(@mui/material@5.15.15)(@types/react@18.2.67)(react@18.2.0): - resolution: {integrity: sha512-kkeU/pe+hABcYDH6Uqy8RmIsr2S/y5bP2rp+Gat4CcRjCcVne6KudS1NrZQhUCRysrTDCAhcbcf9gt+/+pGO2g==} + /@mui/icons-material@5.15.14(@mui/material@5.15.14)(@types/react@18.2.67)(react@18.2.0): + resolution: {integrity: sha512-vj/51k7MdFmt+XVw94sl30SCvGx6+wJLsNYjZRgxhS6y3UtnWnypMOsm3Kmg8TN+P0dqwsjy4/fX7B1HufJIhw==} engines: {node: '>=12.0.0'} peerDependencies: '@mui/material': ^5.0.0 @@ -2519,14 +2527,14 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 - '@mui/material': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@babel/runtime': 7.24.1 + '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.67 react: 18.2.0 dev: false - /@mui/material@5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-3zvWayJ+E1kzoIsvwyEvkTUKVKt1AjchFFns+JtluHCuvxgKcLSRJTADw37k0doaRtVAsyh8bz9Afqzv+KYrIA==} + /@mui/material@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -2542,12 +2550,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 '@emotion/react': 11.11.4(@types/react@18.2.67)(react@18.2.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) '@mui/base': 5.0.0-beta.40(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) - '@mui/core-downloads-tracker': 5.15.15 - '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.67)(react@18.2.0) + '@mui/core-downloads-tracker': 5.15.14 + '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.67)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.67) '@mui/utils': 5.15.14(@types/react@18.2.67)(react@18.2.0) '@types/react': 18.2.67 @@ -2571,14 +2579,14 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 '@mui/utils': 5.15.14(@types/react@18.2.67)(react@18.2.0) '@types/react': 18.2.67 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styled-engine@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0): + /@mui/styled-engine@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0): resolution: {integrity: sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==} engines: {node: '>=12.0.0'} peerDependencies: @@ -2591,17 +2599,17 @@ packages: '@emotion/styled': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 '@emotion/cache': 11.11.0 '@emotion/react': 11.11.4(@types/react@18.2.67)(react@18.2.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/system@5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.67)(react@18.2.0): - resolution: {integrity: sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==} + /@mui/system@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.67)(react@18.2.0): + resolution: {integrity: sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -2616,11 +2624,11 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 '@emotion/react': 11.11.4(@types/react@18.2.67)(react@18.2.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) '@mui/private-theming': 5.15.14(@types/react@18.2.67)(react@18.2.0) - '@mui/styled-engine': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0) + '@mui/styled-engine': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.67) '@mui/utils': 5.15.14(@types/react@18.2.67)(react@18.2.0) '@types/react': 18.2.67 @@ -2651,16 +2659,16 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 - '@types/prop-types': 15.7.12 + '@babel/runtime': 7.24.1 + '@types/prop-types': 15.7.11 '@types/react': 18.2.67 prop-types: 15.8.1 react: 18.2.0 react-is: 18.2.0 dev: false - /@mui/x-date-pickers@7.2.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.15)(@types/react@18.2.67)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-hsXugZ+n1ZnHRYzf7+PFrjZ44T+FyGZmTreBmH0M2RUaAblgK+A1V3KNLT+r4Y9gJLH+92LwePxQ9xyfR+E51A==} + /@mui/x-date-pickers@7.0.0(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(@types/react@18.2.67)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/9mp4O2WMixHOso63DBoZVfJVYGrzOHF5voheV2tYQ4XqDdTKp2AdWS3oh8PGwrsvCzqkvb3quzTqhKoEsJUwA==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.9.0 @@ -2695,12 +2703,12 @@ packages: moment-jalaali: optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 '@emotion/react': 11.11.4(@types/react@18.2.67)(react@18.2.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.67)(react@18.2.0) '@mui/base': 5.0.0-beta.40(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) - '@mui/material': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.67)(react@18.2.0) + '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.67)(react@18.2.0) '@mui/utils': 5.15.14(@types/react@18.2.67)(react@18.2.0) '@types/react-transition-group': 4.4.10 clsx: 2.1.0 @@ -2759,8 +2767,8 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@reduxjs/toolkit@2.2.3(react-redux@9.1.0)(react@18.2.0): - resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==} + /@reduxjs/toolkit@2.2.2(react-redux@9.1.0)(react@18.2.0): + resolution: {integrity: sha512-454GZrEx3G6QSYwIx9ROaso1HR6sTH8qyZBe3KEsdWVGU3ayV8jYCwdaEJV3vl9V6+pi3GRl+7Xl7AeDna6qwQ==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -2837,106 +2845,106 @@ packages: rollup: 2.79.1 dev: false - /@rollup/rollup-android-arm-eabi@4.14.2: - resolution: {integrity: sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==} + /@rollup/rollup-android-arm-eabi@4.14.0: + resolution: {integrity: sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==} cpu: [arm] os: [android] requiresBuild: true optional: true - /@rollup/rollup-android-arm64@4.14.2: - resolution: {integrity: sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==} + /@rollup/rollup-android-arm64@4.14.0: + resolution: {integrity: sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==} cpu: [arm64] os: [android] requiresBuild: true optional: true - /@rollup/rollup-darwin-arm64@4.14.2: - resolution: {integrity: sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==} + /@rollup/rollup-darwin-arm64@4.14.0: + resolution: {integrity: sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@rollup/rollup-darwin-x64@4.14.2: - resolution: {integrity: sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==} + /@rollup/rollup-darwin-x64@4.14.0: + resolution: {integrity: sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==} cpu: [x64] os: [darwin] requiresBuild: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.14.2: - resolution: {integrity: sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==} + /@rollup/rollup-linux-arm-gnueabihf@4.14.0: + resolution: {integrity: sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==} cpu: [arm] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.14.2: - resolution: {integrity: sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==} + /@rollup/rollup-linux-arm64-gnu@4.14.0: + resolution: {integrity: sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-arm64-musl@4.14.2: - resolution: {integrity: sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==} + /@rollup/rollup-linux-arm64-musl@4.14.0: + resolution: {integrity: sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-powerpc64le-gnu@4.14.2: - resolution: {integrity: sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==} - cpu: [ppc64] + /@rollup/rollup-linux-powerpc64le-gnu@4.14.0: + resolution: {integrity: sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==} + cpu: [ppc64le] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.14.2: - resolution: {integrity: sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==} + /@rollup/rollup-linux-riscv64-gnu@4.14.0: + resolution: {integrity: sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==} cpu: [riscv64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-s390x-gnu@4.14.2: - resolution: {integrity: sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==} + /@rollup/rollup-linux-s390x-gnu@4.14.0: + resolution: {integrity: sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==} cpu: [s390x] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-x64-gnu@4.14.2: - resolution: {integrity: sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==} + /@rollup/rollup-linux-x64-gnu@4.14.0: + resolution: {integrity: sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-x64-musl@4.14.2: - resolution: {integrity: sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==} + /@rollup/rollup-linux-x64-musl@4.14.0: + resolution: {integrity: sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.14.2: - resolution: {integrity: sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==} + /@rollup/rollup-win32-arm64-msvc@4.14.0: + resolution: {integrity: sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==} cpu: [arm64] os: [win32] requiresBuild: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.14.2: - resolution: {integrity: sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==} + /@rollup/rollup-win32-ia32-msvc@4.14.0: + resolution: {integrity: sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==} cpu: [ia32] os: [win32] requiresBuild: true optional: true - /@rollup/rollup-win32-x64-msvc@4.14.2: - resolution: {integrity: sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==} + /@rollup/rollup-win32-x64-msvc@4.14.0: + resolution: {integrity: sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==} cpu: [x64] os: [win32] requiresBuild: true @@ -2980,7 +2988,7 @@ packages: /@surma/rollup-plugin-off-main-thread@2.2.3: resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} dependencies: - ejs: 3.1.10 + ejs: 3.1.9 json5: 2.2.3 magic-string: 0.25.9 string.prototype.matchall: 4.0.11 @@ -3118,20 +3126,6 @@ packages: defer-to-connect: 2.0.1 dev: true - /@testing-library/dom@10.0.0: - resolution: {integrity: sha512-PmJPnogldqoVFf+EwbHvbBJ98MmqASV8kLrBYgsDNxQcFMeIS7JFL48sfyXvuMtgmWO/wMhh25odr+8VhDmn4g==} - engines: {node: '>=18'} - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/runtime': 7.24.4 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - dev: true - /@testing-library/dom@9.3.4: resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} engines: {node: '>=14'} @@ -3193,13 +3187,13 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@testing-library/user-event@14.5.2(@testing-library/dom@10.0.0): + /@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4): resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' dependencies: - '@testing-library/dom': 10.0.0 + '@testing-library/dom': 9.3.4 dev: true /@tootallnate/once@2.0.0: @@ -3380,8 +3374,8 @@ packages: dependencies: undici-types: 5.26.5 - /@types/node@20.12.7: - resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + /@types/node@20.12.3: + resolution: {integrity: sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==} dependencies: undici-types: 5.26.5 dev: false @@ -3393,10 +3387,6 @@ packages: /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} - /@types/prop-types@15.7.12: - resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} - dev: false - /@types/react-dom@18.2.22: resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} dependencies: @@ -3428,7 +3418,7 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.3 dev: false /@types/responselike@1.0.3: @@ -3484,7 +3474,7 @@ packages: '@types/yargs-parser': 21.0.3 dev: true - /@typescript-eslint/eslint-plugin@7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/eslint-plugin@7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -3496,10 +3486,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/scope-manager': 7.4.0 - '@typescript-eslint/type-utils': 7.4.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/type-utils': 7.4.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/visitor-keys': 7.4.0 debug: 4.3.4 eslint: 8.57.0 @@ -3507,13 +3497,13 @@ packages: ignore: 5.3.1 natural-compare: 1.4.0 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + ts-api-utils: 1.3.0(typescript@5.4.2) + typescript: 5.4.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@7.4.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/parser@7.4.0(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -3525,11 +3515,11 @@ packages: dependencies: '@typescript-eslint/scope-manager': 7.4.0 '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.2) '@typescript-eslint/visitor-keys': 7.4.0 debug: 4.3.4 eslint: 8.57.0 - typescript: 5.4.5 + typescript: 5.4.2 transitivePeerDependencies: - supports-color dev: true @@ -3542,7 +3532,7 @@ packages: '@typescript-eslint/visitor-keys': 7.4.0 dev: true - /@typescript-eslint/type-utils@7.4.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/type-utils@7.4.0(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -3552,12 +3542,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.5) - '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.2) + '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.2) debug: 4.3.4 eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + ts-api-utils: 1.3.0(typescript@5.4.2) + typescript: 5.4.2 transitivePeerDependencies: - supports-color dev: true @@ -3567,7 +3557,7 @@ packages: engines: {node: ^18.18.0 || >=20.0.0} dev: true - /@typescript-eslint/typescript-estree@7.4.0(typescript@5.4.5): + /@typescript-eslint/typescript-estree@7.4.0(typescript@5.4.2): resolution: {integrity: sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -3583,13 +3573,13 @@ packages: is-glob: 4.0.3 minimatch: 9.0.3 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + ts-api-utils: 1.3.0(typescript@5.4.2) + typescript: 5.4.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@7.4.0(eslint@8.57.0)(typescript@5.4.5): + /@typescript-eslint/utils@7.4.0(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -3600,7 +3590,7 @@ packages: '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 7.4.0 '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.2) eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: @@ -3620,13 +3610,13 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react-swc@3.6.0(vite@5.2.8): + /@vitejs/plugin-react-swc@3.6.0(vite@5.2.6): resolution: {integrity: sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==} peerDependencies: vite: ^4 || ^5 dependencies: '@swc/core': 1.4.8 - vite: 5.2.8(@types/node@20.11.30) + vite: 5.2.6(@types/node@20.11.30) transitivePeerDependencies: - '@swc/helpers' dev: true @@ -4498,7 +4488,7 @@ packages: yaml: 1.10.2 dev: false - /cosmiconfig@9.0.0(typescript@5.4.5): + /cosmiconfig@9.0.0(typescript@5.4.2): resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} peerDependencies: @@ -4511,7 +4501,7 @@ packages: import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 - typescript: 5.4.5 + typescript: 5.4.2 dev: true /create-jest@29.7.0(@types/node@20.11.30)(ts-node@10.9.2): @@ -4796,7 +4786,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 csstype: 3.1.3 dev: false @@ -4842,8 +4832,8 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true - /ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + /ejs@3.1.9: + resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} engines: {node: '>=0.10.0'} hasBin: true dependencies: @@ -5177,7 +5167,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.2) debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -5207,7 +5197,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.2) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -6589,7 +6579,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.2(@types/node@20.11.30)(typescript@5.4.5) + ts-node: 10.9.2(@types/node@20.11.30)(typescript@5.4.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -6895,7 +6885,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.3 merge-stream: 2.0.0 supports-color: 7.2.0 dev: false @@ -7112,14 +7102,14 @@ packages: type-check: 0.4.0 dev: true - /license-compliance@3.0.0(typescript@5.4.5): + /license-compliance@3.0.0(typescript@5.4.2): resolution: {integrity: sha512-0kXEr7JSdP+jPSTSEnAiyGvpOoFnkiVXqmTFhXx22+tCay7shTN1mVM7Z+p2F3YNeIhx0tmADglrp5ddWGyHnQ==} engines: {node: '>=18.20.1'} hasBin: true dependencies: chalk: 4.1.2 commander: 12.0.0 - cosmiconfig: 9.0.0(typescript@5.4.5) + cosmiconfig: 9.0.0(typescript@5.4.2) debug: 4.3.4 joi: 17.12.3 spdx-expression-parse: 4.0.0 @@ -7402,7 +7392,7 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true - /msw@1.3.3(typescript@5.4.5): + /msw@1.3.3(typescript@5.4.2): resolution: {integrity: sha512-CiPyRFiYJCXYyH/vwxT7m+sa4VZHuUH6cGwRBj0kaTjBGpsk4EnL47YzhoA859htVCF2vzqZuOsomIUlFqg9GQ==} engines: {node: '>=14'} hasBin: true @@ -7431,7 +7421,7 @@ packages: path-to-regexp: 6.2.1 strict-event-emitter: 0.4.6 type-fest: 2.19.0 - typescript: 5.4.5 + typescript: 5.4.2 yargs: 17.7.2 transitivePeerDependencies: - encoding @@ -7870,13 +7860,13 @@ packages: fast-diff: 1.3.0 dev: true - /prettier-plugin-sort-imports@1.8.5(typescript@5.4.5): - resolution: {integrity: sha512-PkizzuO2S8h3kJeWHytnMZXqvv/fD6g+en/dhv4y5QjoiMm1wq3FWzFiFT7c/BilX95l0ZIqJTlMsXYs8z/WQQ==} + /prettier-plugin-sort-imports@1.8.4(typescript@5.4.2): + resolution: {integrity: sha512-3Y5TK68TXdP+ViIzRNp4bvjjjPZ0MULL96ImBVTwWtKiIOIcuBIzFmtfPEzOVHaX0tJa3MGChrzmJAsyObvPbA==} peerDependencies: typescript: '>4.0.0' dependencies: prettier: 3.2.5 - typescript: 5.4.5 + typescript: 5.4.2 dev: true /prettier@3.2.5: @@ -7928,6 +7918,10 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 + /property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + dev: false + /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false @@ -8004,6 +7998,15 @@ packages: react: 18.2.0 dev: false + /react-hook-form@7.51.3(react@18.2.0): + resolution: {integrity: sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -8065,7 +8068,7 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -8119,7 +8122,7 @@ packages: /redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 dev: true /redux@5.0.1: @@ -8322,28 +8325,28 @@ packages: fsevents: 2.3.3 dev: false - /rollup@4.14.2: - resolution: {integrity: sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==} + /rollup@4.14.0: + resolution: {integrity: sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.14.2 - '@rollup/rollup-android-arm64': 4.14.2 - '@rollup/rollup-darwin-arm64': 4.14.2 - '@rollup/rollup-darwin-x64': 4.14.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.14.2 - '@rollup/rollup-linux-arm64-gnu': 4.14.2 - '@rollup/rollup-linux-arm64-musl': 4.14.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.14.2 - '@rollup/rollup-linux-riscv64-gnu': 4.14.2 - '@rollup/rollup-linux-s390x-gnu': 4.14.2 - '@rollup/rollup-linux-x64-gnu': 4.14.2 - '@rollup/rollup-linux-x64-musl': 4.14.2 - '@rollup/rollup-win32-arm64-msvc': 4.14.2 - '@rollup/rollup-win32-ia32-msvc': 4.14.2 - '@rollup/rollup-win32-x64-msvc': 4.14.2 + '@rollup/rollup-android-arm-eabi': 4.14.0 + '@rollup/rollup-android-arm64': 4.14.0 + '@rollup/rollup-darwin-arm64': 4.14.0 + '@rollup/rollup-darwin-x64': 4.14.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.14.0 + '@rollup/rollup-linux-arm64-gnu': 4.14.0 + '@rollup/rollup-linux-arm64-musl': 4.14.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.14.0 + '@rollup/rollup-linux-riscv64-gnu': 4.14.0 + '@rollup/rollup-linux-s390x-gnu': 4.14.0 + '@rollup/rollup-linux-x64-gnu': 4.14.0 + '@rollup/rollup-linux-x64-musl': 4.14.0 + '@rollup/rollup-win32-arm64-msvc': 4.14.0 + '@rollup/rollup-win32-ia32-msvc': 4.14.0 + '@rollup/rollup-win32-x64-msvc': 4.14.0 fsevents: 2.3.3 /run-async@2.4.1: @@ -8881,6 +8884,10 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true + /tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + dev: false + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -8902,6 +8909,10 @@ packages: dependencies: is-number: 7.0.0 + /toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: false + /tough-cookie@4.1.3: resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} engines: {node: '>=6'} @@ -8934,16 +8945,16 @@ packages: engines: {node: '>= 14.0.0'} dev: true - /ts-api-utils@1.3.0(typescript@5.4.5): + /ts-api-utils@1.3.0(typescript@5.4.2): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.4.5 + typescript: 5.4.2 dev: true - /ts-jest@29.1.2(@babel/core@7.24.3)(jest@29.7.0)(typescript@5.4.5): + /ts-jest@29.1.2(@babel/core@7.24.3)(jest@29.7.0)(typescript@5.4.2): resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -8973,11 +8984,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.6.0 - typescript: 5.4.5 + typescript: 5.4.2 yargs-parser: 21.1.1 dev: true - /ts-node@10.9.2(@types/node@20.11.30)(typescript@5.4.5): + /ts-node@10.9.2(@types/node@20.11.30)(typescript@5.4.2): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -9003,18 +9014,18 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.4.5 + typescript: 5.4.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true - /tsc-files@1.1.4(typescript@5.4.5): + /tsc-files@1.1.4(typescript@5.4.2): resolution: {integrity: sha512-RePsRsOLru3BPpnf237y1Xe1oCGta8rmSYzM76kYo5tLGsv5R2r3s64yapYorGTPuuLyfS9NVbh9ydzmvNie2w==} hasBin: true peerDependencies: typescript: '>=3' dependencies: - typescript: 5.4.5 + typescript: 5.4.2 dev: true /tsconfig-paths@3.15.0: @@ -9060,7 +9071,6 @@ packages: /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - dev: true /type-fest@3.13.1: resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} @@ -9113,8 +9123,8 @@ packages: is-typedarray: 1.0.0 dev: true - /typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + /typescript@5.4.2: + resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} hasBin: true @@ -9237,7 +9247,7 @@ packages: convert-source-map: 2.0.0 dev: true - /vite-plugin-pwa@0.19.5(vite@5.2.8)(workbox-build@7.0.0)(workbox-window@7.0.0): + /vite-plugin-pwa@0.19.5(vite@5.2.6)(workbox-build@7.0.0)(workbox-window@7.0.0): resolution: {integrity: sha512-3xJEc2Gmq6SBf730UAV1N2/MqOm+MiyvaLToSTglg+pH9b9qm666yPVxrBBlcOhGoJJWjJpu+Z9tROKek2CODg==} engines: {node: '>=16.0.0'} peerDependencies: @@ -9252,15 +9262,15 @@ packages: debug: 4.3.4 fast-glob: 3.3.2 pretty-bytes: 6.1.1 - vite: 5.2.8(@types/node@20.11.30) + vite: 5.2.6(@types/node@20.11.30) workbox-build: 7.0.0 workbox-window: 7.0.0 transitivePeerDependencies: - supports-color dev: false - /vite@5.2.8(@types/node@20.11.30): - resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} + /vite@5.2.6(@types/node@20.11.30): + resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -9290,7 +9300,7 @@ packages: '@types/node': 20.11.30 esbuild: 0.20.2 postcss: 8.4.38 - rollup: 4.14.2 + rollup: 4.14.0 optionalDependencies: fsevents: 2.3.3 @@ -9548,7 +9558,6 @@ packages: /workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} - deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 7.0.0 workbox-core: 7.0.0 @@ -9760,3 +9769,12 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /yup@1.4.0: + resolution: {integrity: sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==} + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + dev: false diff --git a/frontend/src/constants/fileConfig.ts b/frontend/src/constants/fileConfig.ts index 459e33cdff..faa29d14c7 100644 --- a/frontend/src/constants/fileConfig.ts +++ b/frontend/src/constants/fileConfig.ts @@ -1,4 +1,4 @@ -import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { CALENDAR, REWORK_TIME_LIST } from '@src/constants/resources'; import { IReworkConfig } from '@src/context/Metrics/metricsSlice'; diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index 5a06599746..b805715ce6 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -257,10 +257,6 @@ export enum REPORT_SUFFIX_UNITS { export const MESSAGE = { VERIFY_FAILED_ERROR: 'verify failed', - VERIFY_MAIL_FAILED_ERROR: 'Email is incorrect!', - VERIFY_TOKEN_FAILED_ERROR: 'Token is invalid, please change your token with correct access permission!', - VERIFY_SITE_FAILED_ERROR: 'Site is incorrect!', - VERIFY_BOARD_FAILED_ERROR: 'Board Id is incorrect!', UNKNOWN_ERROR: 'Unknown', GET_STEPS_FAILED: 'Failed to get', HOME_VERIFY_IMPORT_WARNING: 'The content of the imported JSON file is empty. Please confirm carefully', @@ -437,8 +433,6 @@ export const TIME_RANGE_TITLE = 'Time range settings'; export const ADD_TIME_RANGE_BUTTON_TEXT = 'New time range'; export const REMOVE_BUTTON_TEXT = 'Remove'; export const MAX_TIME_RANGE_AMOUNT = 6; -export const START_DATE_INVALID_TEXT = 'Start date is invalid'; -export const END_DATE_INVALID_TEXT = 'End date is invalid'; export enum SORTING_DATE_RANGE_TEXT { DEFAULT = 'Default sort', diff --git a/frontend/src/containers/ConfigStep/BasicInfo/RequiredMetrics/index.tsx b/frontend/src/containers/ConfigStep/BasicInfo/RequiredMetrics/index.tsx index 07f490286d..e75fcacb05 100644 --- a/frontend/src/containers/ConfigStep/BasicInfo/RequiredMetrics/index.tsx +++ b/frontend/src/containers/ConfigStep/BasicInfo/RequiredMetrics/index.tsx @@ -1,10 +1,12 @@ import { Checkbox, FormHelperText, InputLabel, ListItemText, MenuItem, Select, SelectChangeEvent } from '@mui/material'; import { RequireDataSelections } from '@src/containers/ConfigStep/BasicInfo/RequiredMetrics/style'; +import { BASIC_INFO_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; import { selectMetrics, updateMetrics } from '@src/context/config/configSlice'; import { useAppDispatch, useAppSelector } from '@src/hooks/useAppDispatch'; +import { METRICS_LITERAL } from '@src/containers/ConfigStep/Form/literal'; import { SELECTED_VALUE_SEPARATOR } from '@src/constants/commons'; +import { Controller, useFormContext } from 'react-hook-form'; import { REQUIRED_DATA } from '@src/constants/resources'; -import { useMemo, useCallback } from 'react'; const ALL = 'All'; const ALL_REQUIRED_DATA = Object.values(REQUIRED_DATA) as string[]; @@ -12,50 +14,55 @@ const ALL_REQUIRED_DATA = Object.values(REQUIRED_DATA) as string[]; export const RequiredMetrics = () => { const dispatch = useAppDispatch(); const metrics = useAppSelector(selectMetrics); - - const isEveryOptionsSelected = useCallback( - (options: string[]) => ALL_REQUIRED_DATA.every((metric) => options.includes(metric)), - [], - ); - - const isAllSelected = useMemo(() => isEveryOptionsSelected(metrics), [isEveryOptionsSelected, metrics]); - - const isClickedAll = (options: string[]) => isEveryOptionsSelected(options) || options[options.length - 1] === ALL; - - const onChange = ({ target: { value: selectedOptions } }: SelectChangeEvent) => { - const nextSelectedOptions = isClickedAll(selectedOptions as string[]) - ? isAllSelected - ? [] - : ALL_REQUIRED_DATA - : selectedOptions; - dispatch(updateMetrics(nextSelectedOptions)); - }; - + const { control } = useFormContext(); const onRender = (selected: string[]) => selected.join(SELECTED_VALUE_SEPARATOR); return ( <> Required metrics - - {metrics.length === 0 && Metrics is required} + { + const isEveryOptionsSelected = ALL_REQUIRED_DATA.every((metric) => field.value.includes(metric)); + const onChange = ({ target: { value: selectedOptions } }: SelectChangeEvent) => { + const isClickingAll = selectedOptions[selectedOptions.length - 1] === ALL; + const nextSelectedOptions = isClickingAll + ? isEveryOptionsSelected + ? [] + : ALL_REQUIRED_DATA + : selectedOptions; + field.onChange(nextSelectedOptions); + dispatch(updateMetrics(nextSelectedOptions)); + }; + return ( + <> + + {field.value.length === 0 && ( + {BASIC_INFO_ERROR_MESSAGE.metrics.required} + )} + + ); + }} + /> ); diff --git a/frontend/src/containers/ConfigStep/BasicInfo/index.tsx b/frontend/src/containers/ConfigStep/BasicInfo/index.tsx index d1ad2bee12..e5b6f3191a 100644 --- a/frontend/src/containers/ConfigStep/BasicInfo/index.tsx +++ b/frontend/src/containers/ConfigStep/BasicInfo/index.tsx @@ -1,59 +1,69 @@ -import { - selectCalendarType, - selectProjectName, - selectWarningMessage, - updateCalendarType, - updateProjectName, -} from '@src/context/config/configSlice'; +import { selectWarningMessage, updateCalendarType, updateProjectName } from '@src/context/config/configSlice'; import { CollectionDateLabel, ProjectNameInput, StyledFormControlLabel } from './style'; import { RequiredMetrics } from '@src/containers/ConfigStep/BasicInfo/RequiredMetrics'; import { DateRangePickerSection } from '@src/containers/ConfigStep/DateRangePicker'; +import { BASIC_INFO_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; import { WarningNotification } from '@src/components/Common/WarningNotification'; import { ConfigSectionContainer } from '@src/components/Common/ConfigForms'; import { useAppDispatch, useAppSelector } from '@src/hooks/useAppDispatch'; import { ConfigSelectionTitle } from '@src/containers/MetricsStep/style'; -import { DEFAULT_HELPER_TEXT } from '@src/constants/commons'; +import { Controller, useFormContext } from 'react-hook-form'; import { CALENDAR } from '@src/constants/resources'; import { Radio, RadioGroup } from '@mui/material'; -import { useState } from 'react'; const BasicInfo = () => { const dispatch = useAppDispatch(); - const projectName = useAppSelector(selectProjectName); - const calendarType = useAppSelector(selectCalendarType); const warningMessage = useAppSelector(selectWarningMessage); - const [isEmptyProjectName, setIsEmptyProjectName] = useState(false); + const { setError, control } = useFormContext(); return ( <> {warningMessage && } Basic information - { - setIsEmptyProjectName(e.target.value === ''); - }} - onChange={(e) => { - dispatch(updateProjectName(e.target.value)); - setIsEmptyProjectName(e.target.value === ''); - }} - error={isEmptyProjectName} - helperText={isEmptyProjectName ? 'Project name is required' : DEFAULT_HELPER_TEXT} + ( + { + dispatch(updateProjectName(e.target.value)); + field.onChange(e.target.value); + }} + onFocus={() => { + if (field.value === '') { + setError('projectName', { message: BASIC_INFO_ERROR_MESSAGE.projectName.required }); + } + }} + error={fieldState.invalid} + helperText={fieldState.error?.message || ''} + /> + )} /> + Collection Date - { - dispatch(updateCalendarType(e.target.value)); + { + return ( + { + field.onChange(e.target.value); + dispatch(updateCalendarType(e.target.value)); + }} + > + } label={CALENDAR.REGULAR} /> + } label={CALENDAR.CHINA} /> + + ); }} - > - } label={CALENDAR.REGULAR} /> - } label={CALENDAR.CHINA} /> - + /> diff --git a/frontend/src/containers/ConfigStep/Board/FormTextField.tsx b/frontend/src/containers/ConfigStep/Board/FormTextField.tsx new file mode 100644 index 0000000000..fd5213c026 --- /dev/null +++ b/frontend/src/containers/ConfigStep/Board/FormTextField.tsx @@ -0,0 +1,66 @@ +import { BOARD_CONFIG_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; +import { IBoardConfigData } from '@src/containers/ConfigStep/Form/schema'; +import { TBoardFieldKeys } from '@src/containers/ConfigStep/Form/type'; +import { StyledTextField } from '@src/components/Common/ConfigForms'; +import { updateBoard } from '@src/context/config/configSlice'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useAppDispatch } from '@src/hooks/useAppDispatch'; +import { KEYS } from '@src/hooks/useVerifyBoardEffect'; +interface IFormTextField { + name: Exclude; + col: number; + label: string; +} + +export const FormTextField = ({ name, col, label }: IFormTextField) => { + const dispatch = useAppDispatch(); + const { + control, + setError, + reset, + formState: { isSubmitSuccessful }, + getValues, + } = useFormContext(); + return ( + { + return ( + { + if (field.value === '') { + setError(name, { message: BOARD_CONFIG_ERROR_MESSAGE[name].required }); + } + }} + onChange={(e) => { + if (isSubmitSuccessful) { + reset(undefined, { keepValues: true, keepErrors: true }); + } + const values = getValues() as IBoardConfigData; + const boardConfig: IBoardConfigData = { + ...values, + [name]: e.target.value, + }; + dispatch(updateBoard(boardConfig)); + field.onChange(e.target.value); + }} + error={fieldState.invalid && fieldState.error?.message !== BOARD_CONFIG_ERROR_MESSAGE.token.timeout} + helperText={ + fieldState.error?.message && fieldState.error?.message !== BOARD_CONFIG_ERROR_MESSAGE.token.timeout + ? fieldState.error?.message + : '' + } + sx={{ gridColumn: `span ${col}` }} + /> + ); + }} + /> + ); +}; diff --git a/frontend/src/containers/ConfigStep/Board/index.tsx b/frontend/src/containers/ConfigStep/Board/index.tsx index ff3406aefc..d3923b9f1b 100644 --- a/frontend/src/containers/ConfigStep/Board/index.tsx +++ b/frontend/src/containers/ConfigStep/Board/index.tsx @@ -1,93 +1,55 @@ -import { - ConfigSectionContainer, - StyledForm, - StyledTextField, - StyledTypeSelections, -} from '@src/components/Common/ConfigForms'; -import { updateShouldGetBoardConfig } from '@src/context/Metrics/metricsSlice'; -import { KEYS, useVerifyBoardEffect } from '@src/hooks/useVerifyBoardEffect'; +import { ConfigSectionContainer, StyledForm } from '@src/components/Common/ConfigForms'; +import { BOARD_CONFIG_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; +import { FIELD_KEY, useVerifyBoardEffect } from '@src/hooks/useVerifyBoardEffect'; +import { FormTextField } from '@src/containers/ConfigStep/Board/FormTextField'; +import { FormSingleSelect } from '@src/containers/ConfigStep/Form/FormSelect'; import { ConfigButtonGrop } from '@src/containers/ConfigStep/ConfigButton'; -import { useAppSelector, useAppDispatch } from '@src/hooks/useAppDispatch'; -import { InputLabel, ListItemText, MenuItem, Select } from '@mui/material'; import { ConfigSelectionTitle } from '@src/containers/MetricsStep/style'; -import { selectIsBoardVerified } from '@src/context/config/configSlice'; import { TimeoutAlert } from '@src/containers/ConfigStep/TimeoutAlert'; import { StyledAlterWrapper } from '@src/containers/ConfigStep/style'; -import { BOARD_TYPES, CONFIG_TITLE } from '@src/constants/resources'; +import { CONFIG_TITLE, BOARD_TYPES } from '@src/constants/resources'; import { Loading } from '@src/components/Loading'; -import { FormEvent, useMemo } from 'react'; +import { useFormContext } from 'react-hook-form'; + export const Board = () => { - const dispatch = useAppDispatch(); - const isVerified = useAppSelector(selectIsBoardVerified); + const { verifyJira, isLoading, fields, resetFields } = useVerifyBoardEffect(); const { - verifyJira, - isLoading, - fields, - updateField, - isShowAlert, - setIsShowAlert, - validateField, - resetFields, - isVerifyTimeOut, - } = useVerifyBoardEffect(); - - const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - await verifyJira(); - dispatch(updateShouldGetBoardConfig(true)); - }; + clearErrors, + formState: { isValid, isSubmitSuccessful, errors }, + handleSubmit, + } = useFormContext(); + const isVerifyTimeOut = errors.token?.message === BOARD_CONFIG_ERROR_MESSAGE.token.timeout; + const isVerified = isValid && isSubmitSuccessful; - const isDisableVerifyButton = useMemo( - () => isLoading || fields.some((field) => !field.value || field.validatedError || field.verifiedError), - [fields, isLoading], - ); + const onSubmit = async () => await verifyJira(); + const closeTimeoutAlert = () => clearErrors(fields[FIELD_KEY.TOKEN].key); return ( {isLoading && } {CONFIG_TITLE.BOARD} - + - - {fields.map(({ key, value, validatedError, verifiedError, col }, index) => - !index ? ( - - Board - - - ) : ( - validateField(key)} - onChange={(e) => updateField(key, e.target.value)} - error={!!validatedError || !!verifiedError} - type={key === KEYS.TOKEN ? 'password' : 'text'} - helperText={validatedError || verifiedError} - sx={{ gridColumn: `span ${col}` }} + + {fields.map(({ key, col, label }) => + key === 'type' ? ( + + ) : ( + ), )} diff --git a/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePicker.tsx b/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePicker.tsx index 418d94d22a..9dae32ca9b 100644 --- a/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePicker.tsx +++ b/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePicker.tsx @@ -4,30 +4,36 @@ import { StyledDateRangePicker, RemoveButton, } from '@src/containers/ConfigStep/DateRangePicker/style'; -import { - DEFAULT_SPRINT_INTERVAL_OFFSET_DAYS, - REMOVE_BUTTON_TEXT, - DATE_RANGE_FORMAT, - START_DATE_INVALID_TEXT, - END_DATE_INVALID_TEXT, -} from '@src/constants/resources'; +import { DEFAULT_SPRINT_INTERVAL_OFFSET_DAYS, REMOVE_BUTTON_TEXT, DATE_RANGE_FORMAT } from '@src/constants/resources'; import { isDateDisabled, calculateLastAvailableDate } from '@src/containers/ConfigStep/DateRangePicker/validation'; +import { BASIC_INFO_ERROR_MESSAGE, AGGREGATED_DATE_ERROR_REASON } from '@src/containers/ConfigStep/Form/literal'; import { IRangePickerProps } from '@src/containers/ConfigStep/DateRangePicker/types'; import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; import { DateValidationError } from '@mui/x-date-pickers'; import { TextField, TextFieldProps } from '@mui/material'; import { Z_INDEX } from '@src/constants/commons'; +import { useFormContext } from 'react-hook-form'; import { Nullable } from '@src/utils/types'; import dayjs, { Dayjs } from 'dayjs'; import isNull from 'lodash/isNull'; -const HelperTextForStartDate = (props: TextFieldProps) => ( - -); +const HelperTextForStartDate = (props: TextFieldProps) => { + const isBlank = props.value === null || props.value === ''; + const isError = props.error || isBlank; + const helperText = isBlank + ? BASIC_INFO_ERROR_MESSAGE.dateRange.startDate.required + : BASIC_INFO_ERROR_MESSAGE.dateRange.startDate.invalid; + return ; +}; -const HelperTextForEndDate = (props: TextFieldProps) => ( - -); +const HelperTextForEndDate = (props: TextFieldProps) => { + const isBlank = props.value === null || props.value === ''; + const isError = props.error || isBlank; + const helperText = isBlank + ? BASIC_INFO_ERROR_MESSAGE.dateRange.endDate.required + : BASIC_INFO_ERROR_MESSAGE.dateRange.endDate.invalid; + return ; +}; export const DateRangePicker = ({ startDate, @@ -42,6 +48,9 @@ export const DateRangePicker = ({ const dateRangeGroupExcludeSelf = rangeList!.filter(({ sortIndex }: { sortIndex: number }) => sortIndex !== index); const shouldStartDateDisableDate = isDateDisabled.bind(null, dateRangeGroupExcludeSelf); const shouldEndDateDisableDate = isDateDisabled.bind(null, dateRangeGroupExcludeSelf); + const startDateFieldName = `dateRange[${index}].startDate`; + const endDateFieldName = `dateRange[${index}].endDate`; + const { setValue } = useFormContext(); const changeStartDate = (value: Nullable, { validationError }: { validationError: DateValidationError }) => { let daysAddToEndDate = DEFAULT_SPRINT_INTERVAL_OFFSET_DAYS; @@ -64,7 +73,18 @@ export const DateRangePicker = ({ startDate: value.startOf('date').format(DATE_RANGE_FORMAT), endDate: value.endOf('date').add(daysAddToEndDate, 'day').format(DATE_RANGE_FORMAT), }; - isNull(validationError) ? onChange?.(result, index) : onError?.('startDateError', validationError, index); + + if (isNull(validationError)) { + if (isNull(value)) { + onError?.('startDateError', BASIC_INFO_ERROR_MESSAGE.dateRange.startDate.required, index); + } + setValue(startDateFieldName, result.startDate, { shouldValidate: true }); + setValue(endDateFieldName, result.endDate, { shouldValidate: true }); + onChange?.(result, index); + } else { + setValue(startDateFieldName, AGGREGATED_DATE_ERROR_REASON, { shouldValidate: true }); + onError?.('startDateError', validationError, index); + } }; const changeEndDate = (value: Nullable, { validationError }: { validationError: DateValidationError }) => { @@ -78,7 +98,17 @@ export const DateRangePicker = ({ endDate: value.endOf('date').format(DATE_RANGE_FORMAT), }; - isNull(validationError) ? onChange?.(result, index) : onError?.('endDateError', validationError, index); + if (isNull(validationError)) { + if (isNull(value)) { + onError?.('endDateError', BASIC_INFO_ERROR_MESSAGE.dateRange.endDate.required, index); + } + setValue(startDateFieldName, result.startDate, { shouldValidate: true }); + setValue(endDateFieldName, result.endDate, { shouldValidate: true }); + onChange?.(result, index); + } else { + setValue(endDateFieldName, AGGREGATED_DATE_ERROR_REASON, { shouldValidate: true }); + onError?.('endDateError', validationError, index); + } }; const removeSelfHandler = () => { @@ -91,15 +121,15 @@ export const DateRangePicker = ({ onError?.('startDateError', err, index)} slots={{ openPickerIcon: CalendarTodayIcon, textField: HelperTextForStartDate, }} slotProps={{ + textField: { required: true }, popper: { sx: { zIndex: Z_INDEX.DROPDOWN }, }, @@ -107,18 +137,18 @@ export const DateRangePicker = ({ /> onError?.('endDateError', err, index)} slots={{ openPickerIcon: CalendarTodayIcon, textField: HelperTextForEndDate, }} slotProps={{ + textField: { required: true }, popper: { sx: { zIndex: Z_INDEX.DROPDOWN }, }, diff --git a/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup.tsx b/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup.tsx index e52bcadb6e..08dfc6cfe8 100644 --- a/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup.tsx +++ b/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup.tsx @@ -1,49 +1,35 @@ import { updateShouldGetBoardConfig, updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; +import { Props, SortedDateRangeType, sortFn } from '@src/containers/ConfigStep/DateRangePicker/types'; import { DateRangePickerGroupContainer } from '@src/containers/ConfigStep/DateRangePicker/style'; import { DateRangePicker } from '@src/containers/ConfigStep/DateRangePicker/DateRangePicker'; import { ADD_TIME_RANGE_BUTTON_TEXT, MAX_TIME_RANGE_AMOUNT } from '@src/constants/resources'; +import { BASIC_INFO_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; import { selectDateRange, updateDateRange } from '@src/context/config/configSlice'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { useAppDispatch, useAppSelector } from '@src/hooks/useAppDispatch'; import { AddButton } from '@src/components/Common/AddButtonOneLine'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DateValidationError } from '@mui/x-date-pickers'; -import { useState, useEffect } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { Nullable } from '@src/utils/types'; +import { useEffect, useState } from 'react'; import sortBy from 'lodash/sortBy'; import remove from 'lodash/remove'; +import isNull from 'lodash/isNull'; import get from 'lodash/get'; -import dayjs from 'dayjs'; -export enum SortType { - DESCENDING = 'DESCENDING', - ASCENDING = 'ASCENDING', - DEFAULT = 'DEFAULT', -} +const deriveErrorMessageByDate = (date: Nullable, message: string) => (isNull(date) ? message : null); -export type SortedDateRangeType = { - startDate: string | null; - endDate: string | null; - sortIndex: number; - startDateError: DateValidationError | null; - endDateError: DateValidationError | null; -}; - -const sortFn = { - DEFAULT: ({ sortIndex }: SortedDateRangeType) => sortIndex, - DESCENDING: ({ startDate }: SortedDateRangeType) => -dayjs(startDate).unix(), - ASCENDING: ({ startDate }: SortedDateRangeType) => dayjs(startDate).unix(), -}; - -type Props = { - sortType: SortType; - onChange?: (data: SortedDateRangeType[]) => void; - onError?: (data: SortedDateRangeType[]) => void; -}; - -const fillDateRangeGroup = (item: T, index: number) => ({ +const fillDateRangeGroup = ( + item: { + startDate: string | null; + endDate: string | null; + }, + index: number, +) => ({ ...item, - startDateError: null, - endDateError: null, + startDateError: deriveErrorMessageByDate(item.startDate, BASIC_INFO_ERROR_MESSAGE.dateRange.startDate.required), + endDateError: deriveErrorMessageByDate(item.endDate, BASIC_INFO_ERROR_MESSAGE.dateRange.endDate.required), sortIndex: index, }); @@ -54,16 +40,18 @@ export const DateRangePickerGroup = ({ sortType, onError }: Props) => { const [sortedDateRangeList, setSortedDateRangeList] = useState( dateRangeGroup.map(fillDateRangeGroup), ); + const { setValue } = useFormContext(); useEffect(() => { - const errors = sortedDateRangeList.filter(({ startDateError, endDateError }) => startDateError || endDateError); - onError?.(errors); + const rangeListWithErrors = sortedDateRangeList.filter( + ({ startDateError, endDateError }) => startDateError || endDateError, + ); + onError?.(rangeListWithErrors); }, [onError, sortedDateRangeList]); - const handleError = (type: string, error: DateValidationError, index: number) => { - setSortedDateRangeList( - sortedDateRangeList.map((item) => ({ ...item, [type]: item.sortIndex === index ? error : null })), - ); + const handleError = (type: string, error: DateValidationError | string, index: number) => { + const newList = sortedDateRangeList.map((item) => ({ ...item, [type]: item.sortIndex === index ? error : null })); + setSortedDateRangeList(newList); }; const dispatchUpdateConfig = () => { @@ -74,6 +62,11 @@ export const DateRangePickerGroup = ({ sortType, onError }: Props) => { const addRangeHandler = () => { const result = [...sortedDateRangeList, { startDate: null, endDate: null }]; setSortedDateRangeList(result.map(fillDateRangeGroup)); + setValue( + `dateRange`, + result.map(({ startDate, endDate }) => ({ startDate, endDate })), + { shouldValidate: true }, + ); dispatch(updateDateRange(result.map(({ startDate, endDate }) => ({ startDate, endDate })))); }; @@ -82,7 +75,15 @@ export const DateRangePickerGroup = ({ sortType, onError }: Props) => { index: number, ) => { const result = sortedDateRangeList.map((item) => - item.sortIndex === index ? { ...item, startDate, endDate, startDateError: null, endDateError: null } : item, + item.sortIndex === index + ? { + ...item, + startDate, + endDate, + startDateError: deriveErrorMessageByDate(startDate, BASIC_INFO_ERROR_MESSAGE.dateRange.startDate.required), + endDateError: deriveErrorMessageByDate(endDate, BASIC_INFO_ERROR_MESSAGE.dateRange.endDate.required), + } + : item, ); setSortedDateRangeList(result); dispatchUpdateConfig(); @@ -92,6 +93,11 @@ export const DateRangePickerGroup = ({ sortType, onError }: Props) => { const handleRemove = (index: number) => { const result = [...sortedDateRangeList]; remove(result, ({ sortIndex }) => sortIndex === index); + setValue( + `dateRange`, + result.map(({ startDate, endDate }) => ({ startDate, endDate })), + { shouldValidate: true }, + ); setSortedDateRangeList(result); dispatchUpdateConfig(); dispatch(updateDateRange(result.map(({ startDate, endDate }) => ({ startDate, endDate })))); diff --git a/frontend/src/containers/ConfigStep/DateRangePicker/SortingDateRange.tsx b/frontend/src/containers/ConfigStep/DateRangePicker/SortingDateRange.tsx index 8caacc973d..3166967ce0 100644 --- a/frontend/src/containers/ConfigStep/DateRangePicker/SortingDateRange.tsx +++ b/frontend/src/containers/ConfigStep/DateRangePicker/SortingDateRange.tsx @@ -5,7 +5,7 @@ import { SortingButtoningContainer, SortingTextButton, } from '@src/containers/ConfigStep/DateRangePicker/style'; -import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { updateDateRangeSortType } from '@src/context/config/configSlice'; import { SORTING_DATE_RANGE_TEXT } from '@src/constants/resources'; import { ArrowDropDown, ArrowDropUp } from '@mui/icons-material'; diff --git a/frontend/src/containers/ConfigStep/DateRangePicker/index.tsx b/frontend/src/containers/ConfigStep/DateRangePicker/index.tsx index 59cf8758a5..c569792c4f 100644 --- a/frontend/src/containers/ConfigStep/DateRangePicker/index.tsx +++ b/frontend/src/containers/ConfigStep/DateRangePicker/index.tsx @@ -1,5 +1,5 @@ -import { DateRangePickerGroup, SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; -import { SortedDateRangeType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; +import { DateRangePickerGroup } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; +import { SortedDateRangeType, SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { SortingDateRange } from '@src/containers/ConfigStep/DateRangePicker/SortingDateRange'; import { selectDateRange, selectDateRangeSortType } from '@src/context/config/configSlice'; import SectionTitleWithTooltip from '@src/components/Common/SectionTitleWithTooltip'; @@ -11,9 +11,7 @@ import { useMemo, useState } from 'react'; export const DateRangePickerSection = () => { const dateRangeGroup = useAppSelector(selectDateRange); const dateRangeGroupSortType = useAppSelector(selectDateRangeSortType); - const [sortType, setSortType] = useState( - dateRangeGroupSortType ? dateRangeGroupSortType : SortType.DEFAULT, - ); + const [sortType, setSortType] = useState(dateRangeGroupSortType); const [hasError, setHasError] = useState(false); const isDateRangeValid = useMemo(() => { diff --git a/frontend/src/containers/ConfigStep/DateRangePicker/types.ts b/frontend/src/containers/ConfigStep/DateRangePicker/types.ts index edac53cfae..31bad4a649 100644 --- a/frontend/src/containers/ConfigStep/DateRangePicker/types.ts +++ b/frontend/src/containers/ConfigStep/DateRangePicker/types.ts @@ -1,13 +1,38 @@ -import { SortedDateRangeType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; import { DateValidationError } from '@mui/x-date-pickers'; +import dayjs from 'dayjs'; + +export type SortedDateRangeType = { + startDate: string | null; + endDate: string | null; + sortIndex: number; + startDateError: DateValidationError | string | null; + endDateError: DateValidationError | string | null; +}; export interface IRangePickerProps { startDate: string | null; endDate: string | null; index: number; key?: string | number; - onError?: (type: string, error: DateValidationError, index: number) => void; + onError?: (type: string, error: DateValidationError | string, index: number) => void; onChange?: (data: { startDate: string | null; endDate: string | null }, index: number) => void; onRemove?: (index: number) => void; rangeList?: SortedDateRangeType[]; } + +export enum SortType { + DESCENDING = 'DESCENDING', + ASCENDING = 'ASCENDING', + DEFAULT = 'DEFAULT', +} + +export const sortFn = { + DEFAULT: ({ sortIndex }: SortedDateRangeType) => sortIndex, + DESCENDING: ({ startDate }: SortedDateRangeType) => -dayjs(startDate).unix(), + ASCENDING: ({ startDate }: SortedDateRangeType) => dayjs(startDate).unix(), +}; +export type Props = { + sortType: SortType; + onChange?: (data: SortedDateRangeType[]) => void; + onError?: (data: SortedDateRangeType[]) => void; +}; diff --git a/frontend/src/containers/ConfigStep/Form/FormSelect.tsx b/frontend/src/containers/ConfigStep/Form/FormSelect.tsx new file mode 100644 index 0000000000..1762a3ccab --- /dev/null +++ b/frontend/src/containers/ConfigStep/Form/FormSelect.tsx @@ -0,0 +1,43 @@ +import { InputLabel, ListItemText, MenuItem, Select } from '@mui/material'; +import { StyledTypeSelections } from '@src/components/Common/ConfigForms'; +import { Controller, useFormContext } from 'react-hook-form'; + +interface IFormSingleSelect { + name: string; + options: string[]; + labelText: string; + labelId?: string; + selectLabelId?: string; + selectAriaLabel?: string; +} + +export const FormSingleSelect = ({ + name, + options, + labelText, + labelId, + selectLabelId, + selectAriaLabel, +}: IFormSingleSelect) => { + const { control } = useFormContext(); + return ( + { + return ( + + {labelText} + + + ); + }} + /> + ); +}; diff --git a/frontend/src/containers/ConfigStep/Form/literal.ts b/frontend/src/containers/ConfigStep/Form/literal.ts new file mode 100644 index 0000000000..b2ac5423c5 --- /dev/null +++ b/frontend/src/containers/ConfigStep/Form/literal.ts @@ -0,0 +1,81 @@ +import { + IBasicInfoErrorMessage, + IBoardConfigErrorMessage, + IPipelineToolErrorMessage, + ISourceControlErrorMessage, +} from '@src/containers/ConfigStep/Form/type'; + +export const AGGREGATED_DATE_ERROR_REASON = 'Invalid date'; +export const CALENDAR_TYPE_LITERAL = ['Regular Calendar(Weekend Considered)', 'Calendar with Chinese Holiday']; +export const METRICS_LITERAL = [ + 'Velocity', + 'Cycle time', + 'Classification', + 'Rework times', + 'Lead time for changes', + 'Deployment frequency', + 'Dev change failure rate', + 'Dev mean time to recovery', +]; +export const BOARD_TYPE_LITERAL = ['Jira']; +export const PIPELINE_TOOL_TYPE_LITERAL = ['BuildKite']; +export const SOURCE_CONTROL_TYPE_LITERAL = ['GitHub']; + +export const BASIC_INFO_ERROR_MESSAGE: IBasicInfoErrorMessage = { + projectName: { + required: 'Project name is required', + }, + metrics: { + required: 'Metrics is required', + }, + dateRange: { + startDate: { + required: 'Start date is required', + invalid: 'Start date is invalid', + }, + endDate: { + required: 'End date is required', + invalid: 'End date is invalid', + }, + }, +}; +export const BOARD_CONFIG_ERROR_MESSAGE: IBoardConfigErrorMessage = { + boardId: { + required: 'Board Id is required!', + invalid: 'Board Id is invalid!', + verifyFailed: 'Board Id is incorrect!', + }, + email: { + required: 'Email is required!', + invalid: 'Email is invalid!', + verifyFailed: 'Email is incorrect!', + }, + site: { + required: 'Site is required!', + verifyFailed: 'Site is incorrect!', + }, + token: { + required: 'Token is required!', + invalid: 'Token is invalid!', + verifyFailed: 'Token is invalid, please change your token with correct access permission!', + timeout: 'Timeout!', + }, +}; +export const PIPELINE_TOOL_ERROR_MESSAGE: IPipelineToolErrorMessage = { + token: { + required: 'Token is required!', + invalid: 'Token is invalid!', + unauthorized: 'Token is incorrect!', + forbidden: 'Forbidden request, please change your token with correct access permission.', + timeout: 'Timeout!', + }, +}; + +export const SOURCE_CONTROL_ERROR_MESSAGE: ISourceControlErrorMessage = { + token: { + required: 'Token is required!', + invalid: 'Token is invalid!', + unauthorized: 'Token is incorrect!', + timeout: 'Timeout!', + }, +}; diff --git a/frontend/src/containers/ConfigStep/Form/schema.ts b/frontend/src/containers/ConfigStep/Form/schema.ts new file mode 100644 index 0000000000..39fa622797 --- /dev/null +++ b/frontend/src/containers/ConfigStep/Form/schema.ts @@ -0,0 +1,101 @@ +import { + CALENDAR_TYPE_LITERAL, + METRICS_LITERAL, + BOARD_TYPE_LITERAL, + PIPELINE_TOOL_TYPE_LITERAL, + SOURCE_CONTROL_TYPE_LITERAL, + BASIC_INFO_ERROR_MESSAGE, + BOARD_CONFIG_ERROR_MESSAGE, + PIPELINE_TOOL_ERROR_MESSAGE, + SOURCE_CONTROL_ERROR_MESSAGE, + AGGREGATED_DATE_ERROR_REASON, +} from '@src/containers/ConfigStep/Form/literal'; +import { object, string, mixed, InferType, array } from 'yup'; +import { REGEX } from '@src/constants/regex'; + +export const basicInfoSchema = object().shape({ + projectName: string().required(BASIC_INFO_ERROR_MESSAGE.projectName.required), + dateRange: array() + .of( + object().shape({ + startDate: string() + .nullable() + .test({ + name: 'CustomStartDateValidation', + test: function (value, context) { + if (value === null) { + return this.createError({ + path: context.path, + message: BASIC_INFO_ERROR_MESSAGE.dateRange.startDate.required, + }); + } + if (value === AGGREGATED_DATE_ERROR_REASON) { + return this.createError({ + path: context.path, + message: BASIC_INFO_ERROR_MESSAGE.dateRange.startDate.invalid, + }); + } else { + return true; + } + }, + }), + endDate: string() + .nullable() + .test({ + name: 'CustomEndDateValidation', + test: function (value, context) { + if (value === null) { + return this.createError({ + path: context.path, + message: BASIC_INFO_ERROR_MESSAGE.dateRange.endDate.required, + }); + } + if (value === AGGREGATED_DATE_ERROR_REASON) { + return this.createError({ + path: context.path, + message: BASIC_INFO_ERROR_MESSAGE.dateRange.endDate.invalid, + }); + } else { + return true; + } + }, + }), + }), + ) + .required(), + calendarType: mixed().oneOf(CALENDAR_TYPE_LITERAL), + metrics: array().of(mixed().oneOf(METRICS_LITERAL)).min(1, BASIC_INFO_ERROR_MESSAGE.metrics.required), +}); + +export const boardConfigSchema = object().shape({ + type: mixed().oneOf(BOARD_TYPE_LITERAL), + boardId: string() + .required(BOARD_CONFIG_ERROR_MESSAGE.boardId.required) + .matches(REGEX.BOARD_ID, { message: BOARD_CONFIG_ERROR_MESSAGE.boardId.invalid }), + email: string() + .required(BOARD_CONFIG_ERROR_MESSAGE.email.required) + .matches(REGEX.EMAIL, { message: BOARD_CONFIG_ERROR_MESSAGE.email.invalid }), + site: string().required(BOARD_CONFIG_ERROR_MESSAGE.site.required), + token: string() + .required(BOARD_CONFIG_ERROR_MESSAGE.token.invalid) + .matches(REGEX.BOARD_TOKEN, { message: BOARD_CONFIG_ERROR_MESSAGE.token.invalid }), +}); + +export const pipelineToolSchema = object().shape({ + type: mixed().oneOf(PIPELINE_TOOL_TYPE_LITERAL), + token: string() + .required(PIPELINE_TOOL_ERROR_MESSAGE.token.required) + .matches(REGEX.BUILDKITE_TOKEN, { message: PIPELINE_TOOL_ERROR_MESSAGE.token.invalid }), +}); + +export const sourceControlSchema = object().shape({ + type: mixed().oneOf(SOURCE_CONTROL_TYPE_LITERAL), + token: string() + .required(SOURCE_CONTROL_ERROR_MESSAGE.token.required) + .matches(REGEX.GITHUB_TOKEN, { message: SOURCE_CONTROL_ERROR_MESSAGE.token.invalid }), +}); + +export type IBasicInfoData = InferType; +export type IBoardConfigData = InferType; +export type IPipelineToolData = InferType; +export type ISourceControlData = InferType; diff --git a/frontend/src/containers/ConfigStep/Form/type.ts b/frontend/src/containers/ConfigStep/Form/type.ts new file mode 100644 index 0000000000..e315cb7094 --- /dev/null +++ b/frontend/src/containers/ConfigStep/Form/type.ts @@ -0,0 +1,69 @@ +export type TBoardFieldKeys = 'type' | 'boardId' | 'email' | 'site' | 'token'; +export type TPipelineToolFieldKeys = 'type' | 'token'; +export type TSourceControlFieldKeys = 'type' | 'token'; +export type TBasicInfoFieldKeys = 'projectName' | 'calendarType' | 'dateRange' | 'metrics'; + +export interface IDateRangeErrorMessage { + startDate: { + required: string; + invalid: string; + }; + endDate: { + required: string; + invalid: string; + }; +} +export interface IBasicInfoErrorMessage + extends Record, Record | IDateRangeErrorMessage> { + projectName: { + required: string; + }; + dateRange: IDateRangeErrorMessage; + metrics: { + required: string; + }; +} +export interface IBoardConfigErrorMessage extends Record, Record> { + boardId: { + required: string; + invalid: string; + verifyFailed: string; + }; + email: { + required: string; + invalid: string; + verifyFailed: string; + }; + site: { + required: string; + verifyFailed: string; + }; + token: { + required: string; + invalid: string; + verifyFailed: string; + timeout: string; + [other: string]: string; + }; +} +export interface IPipelineToolErrorMessage + extends Record, Record> { + token: { + required: string; + invalid: string; + unauthorized: string; + forbidden: string; + timeout: string; + [other: string]: string; + }; +} +export interface ISourceControlErrorMessage + extends Record, Record> { + token: { + required: string; + invalid: string; + unauthorized: string; + timeout: string; + [other: string]: string; + }; +} diff --git a/frontend/src/containers/ConfigStep/Form/useDefaultValues.ts b/frontend/src/containers/ConfigStep/Form/useDefaultValues.ts new file mode 100644 index 0000000000..3bcc57c13a --- /dev/null +++ b/frontend/src/containers/ConfigStep/Form/useDefaultValues.ts @@ -0,0 +1,86 @@ +import { + CALENDAR_TYPE_LITERAL, + BOARD_TYPE_LITERAL, + PIPELINE_TOOL_TYPE_LITERAL, + SOURCE_CONTROL_TYPE_LITERAL, +} from '@src/containers/ConfigStep/Form/literal'; +import { + IBasicInfoData, + IBoardConfigData, + IPipelineToolData, + ISourceControlData, +} from '@src/containers/ConfigStep/Form/schema'; +import { selectBasicInfo, selectBoard, selectPipelineTool, selectSourceControl } from '@src/context/config/configSlice'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; +import { useAppSelector } from '@src/hooks/useAppDispatch'; + +export const basicInfoDefaultValues: IBasicInfoData = { + projectName: '', + dateRange: [], + calendarType: CALENDAR_TYPE_LITERAL[0], + metrics: [], +}; + +export const boardConfigDefaultValues: IBoardConfigData = { + type: BOARD_TYPE_LITERAL[0], + boardId: '', + email: '', + site: '', + token: '', +}; + +export const pipelineToolDefaultValues: IPipelineToolData = { + type: PIPELINE_TOOL_TYPE_LITERAL[0], + token: '', +}; + +export const sourceControlDefaultValues: ISourceControlData = { + type: SOURCE_CONTROL_TYPE_LITERAL[0], + token: '', +}; + +export const useDefaultValues = () => { + const basicInfo = useAppSelector(selectBasicInfo); + const boardConfig = useAppSelector(selectBoard); + const pipelineTool = useAppSelector(selectPipelineTool); + const sourceControl = useAppSelector(selectSourceControl); + + const basicInfoWithImport: IBasicInfoData & { sortType: SortType } = { + ...basicInfoDefaultValues, + projectName: basicInfo.projectName, + calendarType: basicInfo.calendarType, + dateRange: basicInfo.dateRange as { startDate: string; endDate: string }[], + metrics: basicInfo.metrics, + sortType: basicInfo.sortType, + }; + + const boardConfigWithImport: IBoardConfigData = { + ...boardConfigDefaultValues, + type: boardConfig.type, + boardId: boardConfig.boardId, + email: boardConfig.email, + site: boardConfig.site, + token: boardConfig.token, + }; + + const pipelineToolWithImport: IPipelineToolData = { + ...pipelineToolDefaultValues, + ...pipelineTool, + }; + + const sourceControlWithImport: ISourceControlData = { + ...sourceControlDefaultValues, + ...sourceControl, + }; + + return { + basicInfoOriginal: basicInfoDefaultValues, + basicInfoWithImport, + boardConfigOriginal: boardConfigDefaultValues, + boardConfigWithImport, + pipelineToolOriginal: pipelineToolDefaultValues, + pipelineToolWithImport, + sourceControlOriginal: sourceControlDefaultValues, + sourceControlWithImport, + }; +}; diff --git a/frontend/src/containers/ConfigStep/MetricsTypeCheckbox/index.tsx b/frontend/src/containers/ConfigStep/MetricsTypeCheckbox/index.tsx deleted file mode 100644 index 789cdc02f6..0000000000 --- a/frontend/src/containers/ConfigStep/MetricsTypeCheckbox/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { SourceControl } from '@src/containers/ConfigStep/SourceControl'; -import { PipelineTool } from '@src/containers/ConfigStep/PipelineTool'; -import { selectConfig } from '@src/context/config/configSlice'; -import { useAppSelector } from '@src/hooks/useAppDispatch'; -import { Board } from '@src/containers/ConfigStep/Board'; - -export const MetricsTypeCheckbox = () => { - const configData = useAppSelector(selectConfig); - const { isShow: isShowBoard } = configData.board; - const { isShow: isShowPipeline } = configData.pipelineTool; - const { isShow: isShowSourceControl } = configData.sourceControl; - - return ( - <> - {isShowBoard && } - {isShowPipeline && } - {isShowSourceControl && } - - ); -}; diff --git a/frontend/src/containers/ConfigStep/PipelineTool/index.tsx b/frontend/src/containers/ConfigStep/PipelineTool/index.tsx index 5ef8da234b..f40e56e9f8 100644 --- a/frontend/src/containers/ConfigStep/PipelineTool/index.tsx +++ b/frontend/src/containers/ConfigStep/PipelineTool/index.tsx @@ -1,168 +1,99 @@ -import { - isPipelineToolVerified, - selectPipelineTool, - updatePipelineTool, - updatePipelineToolVerifyState, -} from '@src/context/config/configSlice'; -import { - ConfigSectionContainer, - StyledForm, - StyledTextField, - StyledTypeSelections, -} from '@src/components/Common/ConfigForms'; -import { CONFIG_TITLE, PIPELINE_TOOL_TYPES, TOKEN_HELPER_TEXT } from '@src/constants/resources'; -import { useVerifyPipelineToolEffect } from '@src/hooks/useVerifyPipelineToolEffect'; -import { updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; +import { ConfigSectionContainer, StyledForm, StyledTextField } from '@src/components/Common/ConfigForms'; +import { FIELD_KEY, useVerifyPipelineToolEffect } from '@src/hooks/useVerifyPipelineToolEffect'; +import { PIPELINE_TOOL_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; +import { FormSingleSelect } from '@src/containers/ConfigStep/Form/FormSelect'; +import { CONFIG_TITLE, PIPELINE_TOOL_TYPES } from '@src/constants/resources'; import { ConfigButtonGrop } from '@src/containers/ConfigStep/ConfigButton'; -import { useAppDispatch, useAppSelector } from '@src/hooks/useAppDispatch'; -import { DEFAULT_HELPER_TEXT, EMPTY_STRING } from '@src/constants/commons'; -import { InputLabel, ListItemText, MenuItem, Select } from '@mui/material'; +import { IPipelineToolData } from '@src/containers/ConfigStep/Form/schema'; import { ConfigSelectionTitle } from '@src/containers/MetricsStep/style'; import { TimeoutAlert } from '@src/containers/ConfigStep/TimeoutAlert'; import { StyledAlterWrapper } from '@src/containers/ConfigStep/style'; -import { findCaseInsensitiveType } from '@src/utils/util'; -import { FormEvent, useMemo, useState } from 'react'; +import { updatePipelineTool } from '@src/context/config/configSlice'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useAppDispatch } from '@src/hooks/useAppDispatch'; import { Loading } from '@src/components/Loading'; -import { REGEX } from '@src/constants/regex'; - -enum FIELD_KEY { - TYPE = 0, - TOKEN = 1, -} - -const getErrorMessage = (value: string) => { - if (!value) { - return TOKEN_HELPER_TEXT.RequiredTokenText; - } - if (!REGEX.BUILDKITE_TOKEN.test(value.trim())) { - return TOKEN_HELPER_TEXT.InvalidTokenText; - } - return DEFAULT_HELPER_TEXT; -}; export const PipelineTool = () => { const dispatch = useAppDispatch(); - const pipelineToolFields = useAppSelector(selectPipelineTool); - const isVerified = useAppSelector(isPipelineToolVerified); + const { fields, verifyPipelineTool, isLoading, resetFields } = useVerifyPipelineToolEffect(); const { - verifyPipelineTool, - isLoading, - verifiedError, - clearVerifiedError, - isVerifyTimeOut, - isShowAlert, - setIsShowAlert, - } = useVerifyPipelineToolEffect(); - const type = findCaseInsensitiveType(Object.values(PIPELINE_TOOL_TYPES), pipelineToolFields.type); - const [fields, setFields] = useState([ - { - key: 'PipelineTool', - value: type, - validatedError: '', - }, - { - key: 'Token', - value: pipelineToolFields.token, - validatedError: pipelineToolFields.token ? getErrorMessage(pipelineToolFields.token) : '', - }, - ]); - - const handleUpdate = (fields: { key: string; value: string; validatedError: string }[]) => { - clearVerifiedError(); - setFields(fields); - dispatch(updatePipelineToolVerifyState(false)); - dispatch( - updatePipelineTool({ - type: fields[FIELD_KEY.TYPE].value, - token: fields[FIELD_KEY.TOKEN].value, - }), - ); - }; - - const getNewFields = (value: string) => - fields.map((field, index) => - index === FIELD_KEY.TOKEN - ? { - key: field.key, - value: value.trim(), - validatedError: getErrorMessage(value.trim()), - } - : field, - ); - - const onInputUpdate = (value: string) => handleUpdate(getNewFields(value)); - - const onInputFocus = (value: string) => setFields(getNewFields(value)); + control, + setError, + clearErrors, + formState: { isValid, isSubmitSuccessful, errors }, + handleSubmit, + reset, + getValues, + } = useFormContext(); + const isVerifyTimeOut = errors.token?.message === PIPELINE_TOOL_ERROR_MESSAGE.token.timeout; + const isVerified = isValid && isSubmitSuccessful; - const onReset = () => { - const newFields = fields.map(({ key }, index) => ({ - key, - value: index === FIELD_KEY.TYPE ? PIPELINE_TOOL_TYPES.BUILD_KITE : EMPTY_STRING, - validatedError: '', - })); - handleUpdate(newFields); - setIsShowAlert(false); - }; - - const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - await verifyPipelineTool({ - type: fields[FIELD_KEY.TYPE].value, - token: fields[FIELD_KEY.TOKEN].value, - }); - dispatch(updateShouldGetPipelineConfig(true)); - }; - - const isDisableVerifyButton = useMemo( - () => isLoading || fields.some((field) => !field.value || field.validatedError) || !!verifiedError, - [fields, isLoading, verifiedError], - ); + const onSubmit = async () => await verifyPipelineTool(); + const closeTimeoutAlert = () => clearErrors(fields[FIELD_KEY.TOKEN].key); return ( {isLoading && } {CONFIG_TITLE.PIPELINE_TOOL} - + - - - Pipeline Tool - - - onInputFocus(e.target.value)} - onChange={(e) => onInputUpdate(e.target.value)} - error={!!fields[FIELD_KEY.TOKEN].validatedError || !!verifiedError} - helperText={fields[FIELD_KEY.TOKEN].validatedError || verifiedError} + + + { + return ( + { + if (field.value === '') { + setError(fields[FIELD_KEY.TOKEN].key, { + message: PIPELINE_TOOL_ERROR_MESSAGE.token.required, + }); + } + }} + onChange={(e) => { + if (isSubmitSuccessful) { + reset(undefined, { keepValues: true, keepErrors: true }); + } + const pipelineToolConfig: IPipelineToolData = { + ...getValues(), + token: e.target.value, + }; + dispatch(updatePipelineTool(pipelineToolConfig)); + field.onChange(e.target.value); + }} + error={fieldState.invalid && fieldState.error?.message !== PIPELINE_TOOL_ERROR_MESSAGE.token.timeout} + helperText={ + fieldState.error?.message && fieldState.error?.message !== PIPELINE_TOOL_ERROR_MESSAGE.token.timeout + ? fieldState.error?.message + : '' + } + /> + ); + }} /> diff --git a/frontend/src/containers/ConfigStep/SourceControl/index.tsx b/frontend/src/containers/ConfigStep/SourceControl/index.tsx index 39863d0eae..f7503b2ece 100644 --- a/frontend/src/containers/ConfigStep/SourceControl/index.tsx +++ b/frontend/src/containers/ConfigStep/SourceControl/index.tsx @@ -1,157 +1,98 @@ -import { - isSourceControlVerified, - selectSourceControl, - updateSourceControl, - updateSourceControlVerifyState, -} from '@src/context/config/configSlice'; -import { - ConfigSectionContainer, - StyledForm, - StyledTextField, - StyledTypeSelections, -} from '@src/components/Common/ConfigForms'; -import { useVerifySourceControlTokenEffect } from '@src/hooks/useVerifySourceControlTokenEffect'; -import { CONFIG_TITLE, SOURCE_CONTROL_TYPES, TOKEN_HELPER_TEXT } from '@src/constants/resources'; -import { updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; +import { FIELD_KEY, useVerifySourceControlTokenEffect } from '@src/hooks/useVerifySourceControlTokenEffect'; +import { ConfigSectionContainer, StyledForm, StyledTextField } from '@src/components/Common/ConfigForms'; +import { SOURCE_CONTROL_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; +import { FormSingleSelect } from '@src/containers/ConfigStep/Form/FormSelect'; +import { CONFIG_TITLE, SOURCE_CONTROL_TYPES } from '@src/constants/resources'; +import { ISourceControlData } from '@src/containers/ConfigStep/Form/schema'; import { ConfigButtonGrop } from '@src/containers/ConfigStep/ConfigButton'; -import { useAppDispatch, useAppSelector } from '@src/hooks/useAppDispatch'; -import { InputLabel, ListItemText, MenuItem, Select } from '@mui/material'; import { ConfigSelectionTitle } from '@src/containers/MetricsStep/style'; import { TimeoutAlert } from '@src/containers/ConfigStep/TimeoutAlert'; import { StyledAlterWrapper } from '@src/containers/ConfigStep/style'; -import { DEFAULT_HELPER_TEXT } from '@src/constants/commons'; -import { findCaseInsensitiveType } from '@src/utils/util'; -import { FormEvent, useMemo, useState } from 'react'; +import { updateSourceControl } from '@src/context/config/configSlice'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useAppDispatch } from '@src/hooks/useAppDispatch'; import { Loading } from '@src/components/Loading'; -import { REGEX } from '@src/constants/regex'; - -enum FIELD_KEY { - TYPE = 0, - TOKEN = 1, -} - -const getErrorMessage = (value: string) => { - if (!value) { - return TOKEN_HELPER_TEXT.RequiredTokenText; - } - if (!REGEX.GITHUB_TOKEN.test(value.trim())) { - return TOKEN_HELPER_TEXT.InvalidTokenText; - } - return DEFAULT_HELPER_TEXT; -}; export const SourceControl = () => { const dispatch = useAppDispatch(); - const sourceControlFields = useAppSelector(selectSourceControl); - const isVerified = useAppSelector(isSourceControlVerified); - const { verifyToken, isLoading, verifiedError, clearVerifiedError, isVerifyTimeOut, isShowAlert, setIsShowAlert } = - useVerifySourceControlTokenEffect(); - const type = findCaseInsensitiveType(Object.values(SOURCE_CONTROL_TYPES), sourceControlFields.type); - const [fields, setFields] = useState([ - { - key: 'SourceControl', - value: type, - validatedError: '', - }, - { - key: 'Token', - value: sourceControlFields.token, - validatedError: sourceControlFields.token ? getErrorMessage(sourceControlFields.token) : '', - }, - ]); - - const handleUpdate = (fields: { key: string; value: string; validatedError: string }[]) => { - clearVerifiedError(); - setFields(fields); - dispatch(updateSourceControlVerifyState(false)); - dispatch( - updateSourceControl({ - type: fields[FIELD_KEY.TYPE].value, - token: fields[FIELD_KEY.TOKEN].value, - }), - ); - dispatch(updateShouldGetPipelineConfig(true)); - }; - - const getNewFields = (value: string) => - fields.map((field, index) => - index === FIELD_KEY.TOKEN - ? { - key: field.key, - value: value.trim(), - validatedError: getErrorMessage(value.trim()), - } - : field, - ); - - const onInputChange = (value: string) => handleUpdate(getNewFields(value)); - - const onInputFocus = (value: string) => setFields(getNewFields(value)); + const { fields, verifyToken, isLoading, resetFields } = useVerifySourceControlTokenEffect(); + const { + control, + setError, + clearErrors, + formState: { isValid, isSubmitSuccessful, errors }, + handleSubmit, + reset, + getValues, + } = useFormContext(); + const isVerifyTimeOut = errors.token?.message === SOURCE_CONTROL_ERROR_MESSAGE.token.timeout; + const isVerified = isValid && isSubmitSuccessful; - const onReset = () => { - const newFields = fields.map(({ key }, index) => ({ - key, - value: index === FIELD_KEY.TOKEN ? '' : SOURCE_CONTROL_TYPES.GITHUB, - validatedError: '', - })); - handleUpdate(newFields); - setIsShowAlert(false); - }; - - const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - await verifyToken({ - type: fields[FIELD_KEY.TYPE].value as SOURCE_CONTROL_TYPES, - token: fields[FIELD_KEY.TOKEN].value, - }); - }; - - const isDisableVerifyButton = useMemo( - () => isLoading || fields.some((field) => !field.value || field.validatedError) || !!verifiedError, - [verifiedError, fields, isLoading], - ); + const onSubmit = async () => await verifyToken(); + const closeTimeoutAlert = () => clearErrors(fields[FIELD_KEY.TOKEN].key); return ( {isLoading && } {CONFIG_TITLE.SOURCE_CONTROL} - + - - - Source Control - - - onInputFocus(e.target.value)} - onChange={(e) => onInputChange(e.target.value)} - error={!!fields[FIELD_KEY.TOKEN].validatedError || !!verifiedError} - helperText={fields[FIELD_KEY.TOKEN].validatedError || verifiedError} + + + { + return ( + { + if (field.value === '') { + setError(fields[FIELD_KEY.TOKEN].key, { + message: SOURCE_CONTROL_ERROR_MESSAGE.token.required, + }); + } + }} + onChange={(e) => { + if (isSubmitSuccessful) { + reset(undefined, { keepValues: true, keepErrors: true }); + } + const sourceControl: ISourceControlData = { + ...getValues(), + token: e.target.value, + }; + dispatch(updateSourceControl(sourceControl)); + field.onChange(e.target.value); + }} + error={fieldState.invalid && fieldState.error?.message !== SOURCE_CONTROL_ERROR_MESSAGE.token.timeout} + helperText={ + fieldState.error?.message && fieldState.error?.message !== SOURCE_CONTROL_ERROR_MESSAGE.token.timeout + ? fieldState.error?.message + : '' + } + /> + ); + }} /> diff --git a/frontend/src/containers/ConfigStep/TimeoutAlert/index.tsx b/frontend/src/containers/ConfigStep/TimeoutAlert/index.tsx index 4a66619849..f855dcf0f0 100644 --- a/frontend/src/containers/ConfigStep/TimeoutAlert/index.tsx +++ b/frontend/src/containers/ConfigStep/TimeoutAlert/index.tsx @@ -4,22 +4,19 @@ import BoldText from '@src/components/Common/BoldText'; import CancelIcon from '@mui/icons-material/Cancel'; interface PropsInterface { - isVerifyTimeOut: boolean; - isShowAlert: boolean; - setIsShowAlert: (value: boolean) => void; + showAlert: boolean; + onClose: () => void; moduleType: string; } -export const TimeoutAlert = ({ isVerifyTimeOut, isShowAlert, setIsShowAlert, moduleType }: PropsInterface) => { +export const TimeoutAlert = ({ showAlert, onClose, moduleType }: PropsInterface) => { return ( <> - {isVerifyTimeOut && isShowAlert && ( + {showAlert && ( } severity='error' - onClose={() => { - setIsShowAlert(false); - }} + onClose={onClose} > Submission timeout on {moduleType}, please reverify! diff --git a/frontend/src/containers/ConfigStep/index.tsx b/frontend/src/containers/ConfigStep/index.tsx index 327f131468..7ab849a79a 100644 --- a/frontend/src/containers/ConfigStep/index.tsx +++ b/frontend/src/containers/ConfigStep/index.tsx @@ -1,22 +1,69 @@ -import { MetricsTypeCheckbox } from '@src/containers/ConfigStep/MetricsTypeCheckbox'; +import { + IBasicInfoData, + IBoardConfigData, + IPipelineToolData, + ISourceControlData, +} from '@src/containers/ConfigStep/Form/schema'; import { closeAllNotifications } from '@src/context/notification/NotificationSlice'; +import { useAppSelector, useAppDispatch } from '@src/hooks/useAppDispatch'; +import { SourceControl } from '@src/containers/ConfigStep/SourceControl'; +import { PipelineTool } from '@src/containers/ConfigStep/PipelineTool'; +import { selectConfig } from '@src/context/config/configSlice'; +import { FormProvider, UseFormReturn } from 'react-hook-form'; import BasicInfo from '@src/containers/ConfigStep/BasicInfo'; -import { useAppDispatch } from '@src/hooks/useAppDispatch'; +import { Board } from '@src/containers/ConfigStep/Board'; +import { useEffect, useLayoutEffect } from 'react'; import { ConfigStepWrapper } from './style'; -import { useLayoutEffect } from 'react'; -const ConfigStep = () => { - const dispatch = useAppDispatch(); +interface IConfigStepProps { + basicInfoMethods: UseFormReturn; + boardConfigMethods: UseFormReturn; + pipelineToolMethods: UseFormReturn; + sourceControlMethods: UseFormReturn; +} +const ConfigStep = ({ + basicInfoMethods, + boardConfigMethods, + pipelineToolMethods, + sourceControlMethods, +}: IConfigStepProps) => { + const dispatch = useAppDispatch(); useLayoutEffect(() => { dispatch(closeAllNotifications()); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const configData = useAppSelector(selectConfig); + const { isShow: isShowBoard } = configData.board; + const { isShow: isShowPipeline } = configData.pipelineTool; + const { isShow: isShowSourceControl } = configData.sourceControl; + + useEffect(() => { + basicInfoMethods.trigger(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( - - + + + + {isShowBoard && ( + + + + )} + {isShowPipeline && ( + + + + )} + {isShowSourceControl && ( + + + + )} ); }; diff --git a/frontend/src/containers/MetricsStep/Advance/Advance.tsx b/frontend/src/containers/MetricsStep/Advance/Advance.tsx index aef862eeda..d9c5c4a043 100644 --- a/frontend/src/containers/MetricsStep/Advance/Advance.tsx +++ b/frontend/src/containers/MetricsStep/Advance/Advance.tsx @@ -4,11 +4,19 @@ import { ItemCheckbox, TitleAndTooltipContainer, TooltipContainer } from '../Cyc import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; import { StyledLink } from '@src/containers/MetricsStep/style'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; -import { Field } from '@src/hooks/useVerifyBoardEffect'; import { useAppSelector } from '@src/hooks'; import { TextField } from '@mui/material'; import React, { useState } from 'react'; +export interface Field { + key: string; + value: string; + validateRule?: (value: string) => boolean; + validatedError: string; + verifiedError: string; + col: number; +} + export const Advance = () => { const url = 'https://github.com/au-heartbeat/Heartbeat/blob/main/README.md#323-setting-advanced-setting'; const dispatch = useAppDispatch(); diff --git a/frontend/src/containers/MetricsStep/index.tsx b/frontend/src/containers/MetricsStep/index.tsx index 1580260ef8..af189434ac 100644 --- a/frontend/src/containers/MetricsStep/index.tsx +++ b/frontend/src/containers/MetricsStep/index.tsx @@ -1,13 +1,3 @@ -import { - selectBoard, - selectDateRange, - selectIsProjectCreated, - selectJiraColumns, - selectMetrics, - selectUsers, - updateBoardVerifyState, - updateJiraVerifyResponse, -} from '@src/context/config/configSlice'; import { selectMetricsContent, selectShouldGetBoardConfig, @@ -15,6 +5,15 @@ import { updateMetricsState, updateShouldGetBoardConfig, } from '@src/context/Metrics/metricsSlice'; +import { + selectDateRange, + selectIsProjectCreated, + selectMetrics, + selectBoard, + updateJiraVerifyResponse, + selectUsers, + selectJiraColumns, +} from '@src/context/config/configSlice'; import { MetricSelectionHeader, MetricSelectionWrapper, @@ -82,7 +81,6 @@ const MetricsStep = () => { }).then((res) => { if (res && res.length) { const commonPayload = combineBoardInfo(res); - dispatch(updateBoardVerifyState(true)); dispatch(updateJiraVerifyResponse(commonPayload)); dispatch(updateMetricsState(merge(commonPayload, { isProjectCreated: isProjectCreated }))); dispatch(updateShouldGetBoardConfig(false)); diff --git a/frontend/src/containers/MetricsStepper/index.tsx b/frontend/src/containers/MetricsStepper/index.tsx index 4e823172bb..2b5de424c2 100644 --- a/frontend/src/containers/MetricsStepper/index.tsx +++ b/frontend/src/containers/MetricsStepper/index.tsx @@ -1,3 +1,13 @@ +import { + basicInfoSchema, + boardConfigSchema, + pipelineToolSchema, + sourceControlSchema, + IBasicInfoData, + IBoardConfigData, + IPipelineToolData, + ISourceControlData, +} from '@src/containers/ConfigStep/Form/schema'; import { BackButton, ButtonContainer, @@ -18,14 +28,17 @@ import { backStep, nextStep, selectStepNumber, updateTimeStamp } from '@src/cont import { useMetricsStepValidationCheckContext } from '@src/hooks/useMetricsStepValidationCheckContext'; import { convertCycleTimeSettings, exportToJsonFile, onlyEmptyAndDoneState } from '@src/utils/util'; import { selectConfig, selectMetrics, selectPipelineList } from '@src/context/config/configSlice'; +import { useDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; import { COMMON_BUTTONS, METRICS_STEPS, STEPS } from '@src/constants/commons'; import { ConfirmDialog } from '@src/containers/MetricsStepper/ConfirmDialog'; import { useAppDispatch, useAppSelector } from '@src/hooks/useAppDispatch'; import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { getFormMeta } from '@src/context/meta/metaSlice'; import SaveAltIcon from '@mui/icons-material/SaveAlt'; +import { yupResolver } from '@hookform/resolvers/yup'; import { useNavigate } from 'react-router-dom'; import { ROUTE } from '@src/constants/router'; +import { useForm } from 'react-hook-form'; import { Tooltip } from '@mui/material'; import isEmpty from 'lodash/isEmpty'; import every from 'lodash/every'; @@ -49,10 +62,78 @@ const MetricsStepper = () => { const { getDuplicatedPipeLineIds } = useMetricsStepValidationCheckContext(); const formMeta = useAppSelector(getFormMeta); const pipelineList = useAppSelector(selectPipelineList); + const defaultValues = useDefaultValues(); + const { isShow: isShowBoard } = config.board; + const { isShow: isShowPipeline } = config.pipelineTool; + const { isShow: isShowSourceControl } = config.sourceControl; + + const basicInfoMethods = useForm({ + defaultValues: defaultValues.basicInfoWithImport, + resolver: yupResolver(basicInfoSchema), + mode: 'onChange', + }); + + const boardConfigMethods = useForm({ + defaultValues: defaultValues.boardConfigWithImport, + resolver: yupResolver(boardConfigSchema), + mode: 'onChange', + }); + + const pipelineToolMethods = useForm({ + defaultValues: defaultValues.pipelineToolWithImport, + resolver: yupResolver(pipelineToolSchema), + mode: 'onChange', + }); + + const sourceControlMethods = useForm({ + defaultValues: defaultValues.sourceControlWithImport, + resolver: yupResolver(sourceControlSchema), + mode: 'onChange', + }); + + useEffect(() => { + basicInfoMethods.trigger(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const { isValid: isBasicInfoValid } = basicInfoMethods.formState; + const { isValid: isBoardConfigValid, isSubmitSuccessful: isBoardConfigSubmitSuccessful } = + boardConfigMethods.formState; + const { isValid: isPipelineToolValid, isSubmitSuccessful: isPipelineToolSubmitSuccessful } = + pipelineToolMethods.formState; + const { isValid: isSourceControlValid, isSubmitSuccessful: isSourceControlSubmitSuccessful } = + sourceControlMethods.formState; + + const configPageFormMeta = useMemo( + () => [ + { isShow: isShowBoard, isValid: isBoardConfigValid, isSubmitSuccessful: isBoardConfigSubmitSuccessful }, + { isShow: isShowPipeline, isValid: isPipelineToolValid, isSubmitSuccessful: isPipelineToolSubmitSuccessful }, + { + isShow: isShowSourceControl, + isValid: isSourceControlValid, + isSubmitSuccessful: isSourceControlSubmitSuccessful, + }, + ], + [ + isShowBoard, + isBoardConfigValid, + isBoardConfigSubmitSuccessful, + isShowPipeline, + isPipelineToolValid, + isPipelineToolSubmitSuccessful, + isShowSourceControl, + isSourceControlValid, + isSourceControlSubmitSuccessful, + ], + ); + const activeFormMeta = useMemo(() => configPageFormMeta.filter(({ isShow }) => isShow), [configPageFormMeta]); + const shownFormsVerified = useMemo( + () => + activeFormMeta.length > 0 && + activeFormMeta.every(({ isValid, isSubmitSuccessful }) => isValid && isSubmitSuccessful), + [activeFormMeta], + ); - const { isShow: isShowBoard, isVerified: isBoardVerified } = config.board; - const { isShow: isShowPipeline, isVerified: isPipelineToolVerified } = config.pipelineTool; - const { isShow: isShowSourceControl, isVerified: isSourceControlVerified } = config.sourceControl; const isShowCycleTimeSettings = requiredData.includes(REQUIRED_DATA.CYCLE_TIME) || requiredData.includes(REQUIRED_DATA.CLASSIFICATION) || @@ -100,18 +181,6 @@ const MetricsStepper = () => { }, [pipelineList, formMeta.metrics.pipelines, getDuplicatedPipeLineIds, metricsConfig.deploymentFrequencySettings]); useEffect(() => { - if (activeStep === METRICS_STEPS.CONFIG) { - const nextButtonValidityOptions = [ - { isShow: isShowBoard, isValid: isBoardVerified }, - { isShow: isShowPipeline, isValid: isPipelineToolVerified }, - { isShow: isShowSourceControl, isValid: isSourceControlVerified }, - ]; - const activeNextButtonValidityOptions = nextButtonValidityOptions.filter(({ isShow }) => isShow); - projectName && dateRange && dateRange.length && metrics.length - ? setIsDisableNextButton(!activeNextButtonValidityOptions.every(({ isValid }) => isValid)) - : setIsDisableNextButton(true); - } - if (activeStep === METRICS_STEPS.METRICS) { const nextButtonValidityOptions = [ { isShow: isShowBoard, isValid: isCrewsSettingValid }, @@ -131,12 +200,9 @@ const MetricsStepper = () => { } }, [ activeStep, - isBoardVerified, - isPipelineToolVerified, isShowBoard, isShowSourceControl, isShowPipeline, - isSourceControlVerified, metrics, projectName, dateRange, @@ -156,6 +222,9 @@ const MetricsStepper = () => { onlyIncludeReworkMetrics, ]); + const isNextDisabledTempForFormRefactor = + activeStep === METRICS_STEPS.CONFIG ? !(isBasicInfoValid && shownFormsVerified) : isDisableNextButton; + const filterMetricsConfig = (metricsConfig: ISavedMetricsSettingState) => { return Object.fromEntries( Object.entries(metricsConfig).filter(([, value]) => { @@ -261,7 +330,14 @@ const MetricsStepper = () => { - {activeStep === METRICS_STEPS.CONFIG && } + {activeStep === METRICS_STEPS.CONFIG && ( + + )} {activeStep === METRICS_STEPS.METRICS && } {activeStep === METRICS_STEPS.REPORT && } @@ -278,7 +354,7 @@ const MetricsStepper = () => { {COMMON_BUTTONS.BACK} - + {COMMON_BUTTONS.NEXT} diff --git a/frontend/src/context/config/board/boardSlice.ts b/frontend/src/context/config/board/boardSlice.ts index ba01fc7718..a4fff1aff2 100644 --- a/frontend/src/context/config/board/boardSlice.ts +++ b/frontend/src/context/config/board/boardSlice.ts @@ -14,7 +14,6 @@ export interface IBoardState { site: string; token: string; }; - isVerified: boolean; isShow: boolean; verifiedResponse: IBoardVerifyResponseState; } @@ -34,7 +33,6 @@ export const initialBoardState: IBoardState = { site: '', token: '', }, - isVerified: false, isShow: false, verifiedResponse: initialVerifiedBoardState, }; diff --git a/frontend/src/context/config/configSlice.ts b/frontend/src/context/config/configSlice.ts index 3d1af27439..24b7aff82c 100644 --- a/frontend/src/context/config/configSlice.ts +++ b/frontend/src/context/config/configSlice.ts @@ -9,10 +9,10 @@ import { } from '@src/constants/resources'; import { initialPipelineToolState, IPipelineToolState } from '@src/context/config/pipelineTool/pipelineToolSlice'; import { initialSourceControlState, ISourceControl } from '@src/context/config/sourceControl/sourceControlSlice'; -import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup'; import { IBoardState, initialBoardState } from '@src/context/config/board/boardSlice'; import { pipeline } from '@src/context/config/pipelineTool/verifyResponseSlice'; import { uniqPipelineListCrews, updateResponseCrews } from '@src/utils/util'; +import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from '@src/store'; import merge from 'lodash/merge'; @@ -50,7 +50,7 @@ export const initialBasicConfigState: BasicConfigState = { endDate: null, }, ], - sortType: SortType?.DEFAULT, + sortType: SortType.DEFAULT, metrics: [], }, board: initialBoardState, @@ -144,16 +144,13 @@ export const configSlice = createSlice({ ? null : MESSAGE.CONFIG_PAGE_VERIFY_IMPORT_ERROR; } - state.board.config = merge(action.payload.board, { type: 'jira' }); + state.board.config = merge(action.payload.board, { type: 'Jira' }); state.pipelineTool.config = action.payload.pipelineTool || state.pipelineTool.config; state.sourceControl.config = action.payload.sourceControl || state.sourceControl.config; }, updateProjectCreatedState: (state, action) => { state.isProjectCreated = action.payload; }, - updateBoardVerifyState: (state, action) => { - state.board.isVerified = action.payload; - }, updateBoard: (state, action) => { state.board.config = action.payload; }, @@ -163,10 +160,6 @@ export const configSlice = createSlice({ state.board.verifiedResponse.targetFields = targetFields; state.board.verifiedResponse.users = users; }, - - updatePipelineToolVerifyState: (state, action) => { - state.pipelineTool.isVerified = action.payload; - }, updatePipelineTool: (state, action) => { state.pipelineTool.config = action.payload; }, @@ -191,9 +184,6 @@ export const configSlice = createSlice({ : pipeline, ); }, - updateSourceControlVerifyState: (state, action) => { - state.sourceControl.isVerified = action.payload; - }, updateSourceControl: (state, action) => { state.sourceControl.config = action.payload; }, @@ -220,22 +210,18 @@ export const { updateDateRangeSortType, updateMetrics, updateBoard, - updateBoardVerifyState, updateJiraVerifyResponse, updateBasicConfigState, - updatePipelineToolVerifyState, updatePipelineTool, updatePipelineToolVerifyResponse, updateSourceControl, - updateSourceControlVerifyState, updateSourceControlVerifiedResponse, updatePipelineToolVerifyResponseSteps, resetImportedData, updatePipelineToolVerifyResponseCrews, } = configSlice.actions; -export const selectProjectName = (state: RootState) => state.config.basic.projectName; -export const selectCalendarType = (state: RootState) => state.config.basic.calendarType; +export const selectBasicInfo = (state: RootState) => state.config.basic; export const selectDateRange = (state: RootState) => state.config.basic.dateRange; export const selectDateRangeSortType = (state: RootState) => state.config.basic.sortType; export const selectMetrics = (state: RootState) => state.config.basic.metrics; @@ -246,15 +232,10 @@ export const isSelectDoraMetrics = (state: RootState) => export const isOnlySelectClassification = (state: RootState) => state.config.basic.metrics.length === 1 && state.config.basic.metrics[0] === REQUIRED_DATA.CLASSIFICATION; export const selectBoard = (state: RootState) => state.config.board.config; -export const isPipelineToolVerified = (state: RootState) => state.config.pipelineTool.isVerified; export const selectPipelineTool = (state: RootState) => state.config.pipelineTool.config; -export const isSourceControlVerified = (state: RootState) => state.config.sourceControl.isVerified; export const selectSourceControl = (state: RootState) => state.config.sourceControl.config; export const selectWarningMessage = (state: RootState) => state.config.warningMessage; - export const selectConfig = (state: RootState) => state.config; - -export const selectIsBoardVerified = (state: RootState) => state.config.board.isVerified; export const selectUsers = (state: RootState) => state.config.board.verifiedResponse.users; export const selectJiraColumns = (state: RootState) => state.config.board.verifiedResponse.jiraColumns; export const selectIsProjectCreated = (state: RootState) => state.config.isProjectCreated; diff --git a/frontend/src/context/config/pipelineTool/pipelineToolSlice.ts b/frontend/src/context/config/pipelineTool/pipelineToolSlice.ts index 2179ae7994..010826e2c4 100644 --- a/frontend/src/context/config/pipelineTool/pipelineToolSlice.ts +++ b/frontend/src/context/config/pipelineTool/pipelineToolSlice.ts @@ -3,7 +3,6 @@ import { PIPELINE_TOOL_TYPES } from '@src/constants/resources'; export interface IPipelineToolState { config: { type: string; token: string }; - isVerified: boolean; isShow: boolean; verifiedResponse: IPipelineToolVerifyResponse; } @@ -13,7 +12,6 @@ export const initialPipelineToolState: IPipelineToolState = { type: PIPELINE_TOOL_TYPES.BUILD_KITE, token: '', }, - isVerified: false, isShow: false, verifiedResponse: initialPipelineToolVerifiedResponseState, }; diff --git a/frontend/src/context/config/sourceControl/sourceControlSlice.ts b/frontend/src/context/config/sourceControl/sourceControlSlice.ts index 06cb9dae12..c5d0a3bbeb 100644 --- a/frontend/src/context/config/sourceControl/sourceControlSlice.ts +++ b/frontend/src/context/config/sourceControl/sourceControlSlice.ts @@ -3,7 +3,6 @@ import { SOURCE_CONTROL_TYPES } from '@src/constants/resources'; export interface ISourceControl { config: { type: string; token: string }; - isVerified: boolean; isShow: boolean; verifiedResponse: ISourceControlVerifyResponse; } @@ -13,7 +12,6 @@ export const initialSourceControlState: ISourceControl = { type: SOURCE_CONTROL_TYPES.GITHUB, token: '', }, - isVerified: false, isShow: false, verifiedResponse: initSourceControlVerifyResponseState, }; diff --git a/frontend/src/hooks/useGetPipelineToolInfoEffect.ts b/frontend/src/hooks/useGetPipelineToolInfoEffect.ts index 37320c7e83..26138168c9 100644 --- a/frontend/src/hooks/useGetPipelineToolInfoEffect.ts +++ b/frontend/src/hooks/useGetPipelineToolInfoEffect.ts @@ -1,6 +1,5 @@ import { updatePipelineToolVerifyResponse, - isPipelineToolVerified, selectIsProjectCreated, selectPipelineTool, } from '@src/context/config/configSlice'; @@ -28,7 +27,6 @@ export const useGetPipelineToolInfoEffect = (): IUseVerifyPipeLineToolStateInter const [isLoading, setIsLoading] = useState(false); const apiTouchedRef = useRef(false); const [info, setInfo] = useState(defaultInfoStructure); - const pipelineToolVerified = useAppSelector(isPipelineToolVerified); const isProjectCreated = useAppSelector(selectIsProjectCreated); const restoredPipelineTool = useAppSelector(selectPipelineTool); const shouldLoad = useAppSelector(shouldMetricsLoad); @@ -45,12 +43,12 @@ export const useGetPipelineToolInfoEffect = (): IUseVerifyPipeLineToolStateInter const response = await pipelineToolClient.getInfo(params); setInfo(response); dispatch(updatePipelineToolVerifyResponse(response.data)); - pipelineToolVerified && dispatch(updatePipelineSettings({ ...response.data, isProjectCreated })); + dispatch(updatePipelineSettings({ ...response.data, isProjectCreated })); } finally { setIsLoading(false); setIsFirstFetch(false); } - }, [dispatch, isProjectCreated, pipelineToolVerified, restoredPipelineTool.type, restoredPipelineTool.token]); + }, [dispatch, isProjectCreated, restoredPipelineTool.type, restoredPipelineTool.token]); useEffect(() => { if (!apiTouchedRef.current && !isLoading && shouldLoad && shouldGetPipelineConfig) { diff --git a/frontend/src/hooks/useVerifyBoardEffect.ts b/frontend/src/hooks/useVerifyBoardEffect.ts index d44643a4f2..6f5e3f4796 100644 --- a/frontend/src/hooks/useVerifyBoardEffect.ts +++ b/frontend/src/hooks/useVerifyBoardEffect.ts @@ -1,35 +1,40 @@ -import { BOARD_TYPES, AXIOS_REQUEST_ERROR_CODE, MESSAGE, UNKNOWN_ERROR_TITLE } from '@src/constants/resources'; -import { selectBoard, updateBoard, updateBoardVerifyState } from '@src/context/config/configSlice'; -import { findCaseInsensitiveType, getJiraBoardToken } from '@src/utils/util'; -import { useAppDispatch, useAppSelector } from '@src/hooks/useAppDispatch'; -import { DEFAULT_HELPER_TEXT, EMPTY_STRING } from '@src/constants/commons'; +import { AXIOS_REQUEST_ERROR_CODE, UNKNOWN_ERROR_TITLE } from '@src/constants/resources'; +import { BOARD_CONFIG_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; +import { useDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; +import { updateTreatFlagCardAsBlock } from '@src/context/Metrics/metricsSlice'; +import { updateShouldGetBoardConfig } from '@src/context/Metrics/metricsSlice'; +import { IBoardConfigData } from '@src/containers/ConfigStep/Form/schema'; +import { TBoardFieldKeys } from '@src/containers/ConfigStep/Form/type'; import { BoardRequestDTO } from '@src/clients/board/dto/request'; +import { updateBoard } from '@src/context/config/configSlice'; import { boardClient } from '@src/clients/board/BoardClient'; +import { useAppDispatch } from '@src/hooks/useAppDispatch'; +import { getJiraBoardToken } from '@src/utils/util'; import { IAppError } from '@src/errors/ErrorType'; -import { REGEX } from '@src/constants/regex'; +import { useFormContext } from 'react-hook-form'; import { isAppError } from '@src/errors'; import { HttpStatusCode } from 'axios'; import { useState } from 'react'; -export interface Field { - key: string; - value: string; - validateRule?: (value: string) => boolean; - validatedError: string; - verifiedError: string; +export enum FIELD_KEY { + TYPE = 0, + BOARD_ID = 1, + EMAIL = 2, + SITE = 3, + TOKEN = 4, +} + +export interface IField { + key: TBoardFieldKeys; col: number; + label: string; } export interface useVerifyBoardStateInterface { - isVerifyTimeOut: boolean; verifyJira: () => Promise; isLoading: boolean; - fields: Field[]; - updateField: (key: string, value: string) => void; - validateField: (key: string) => void; + fields: IField[]; resetFields: () => void; - setIsShowAlert: (value: boolean) => void; - isShowAlert: boolean; } const ERROR_INFO = { @@ -37,186 +42,85 @@ const ERROR_INFO = { BOARD_NOT_FOUND: 'boardId is incorrect', }; -const VALIDATOR = { - EMAIL: (value: string) => REGEX.EMAIL.test(value), - TOKEN: (value: string) => REGEX.BOARD_TOKEN.test(value), - BOARD_ID: (value: string) => REGEX.BOARD_ID.test(value), -}; - -export const KEYS = { - BOARD: 'Board', - BOARD_ID: 'Board Id', - EMAIL: 'Email', - SITE: 'Site', - TOKEN: 'Token', -}; - -const getValidatedError = (key: string, value: string, validateRule?: (value: string) => boolean) => { - if (!value) { - return `${key} is required!`; - } - if (validateRule && !validateRule(value)) { - return `${key} is invalid!`; - } - return DEFAULT_HELPER_TEXT; +export const KEYS: { [key: string]: TBoardFieldKeys } = { + BOARD: 'type', + BOARD_ID: 'boardId', + EMAIL: 'email', + SITE: 'site', + TOKEN: 'token', }; export const useVerifyBoardEffect = (): useVerifyBoardStateInterface => { const [isLoading, setIsLoading] = useState(false); - const [isVerifyTimeOut, setIsVerifyTimeOut] = useState(false); - const [isShowAlert, setIsShowAlert] = useState(false); - const boardFields = useAppSelector(selectBoard); const dispatch = useAppDispatch(); - const type = findCaseInsensitiveType(Object.values(BOARD_TYPES), boardFields.type); - const [fields, setFields] = useState([ + const { boardConfigOriginal } = useDefaultValues(); + const { reset, setError, getValues } = useFormContext(); + + const originalFields: IField[] = [ { key: KEYS.BOARD, - value: type, - validatedError: '', - verifiedError: '', col: 1, + label: 'Board', }, { key: KEYS.BOARD_ID, - value: boardFields.boardId, - validateRule: VALIDATOR.BOARD_ID, - validatedError: boardFields.boardId - ? getValidatedError(KEYS.BOARD_ID, boardFields.boardId, VALIDATOR.BOARD_ID) - : '', - verifiedError: '', col: 1, + label: 'Board Id', }, { key: KEYS.EMAIL, - value: boardFields.email, - validateRule: VALIDATOR.EMAIL, - validatedError: boardFields.email ? getValidatedError(KEYS.EMAIL, boardFields.email, VALIDATOR.EMAIL) : '', - verifiedError: '', col: 1, + label: 'Email', }, { key: KEYS.SITE, - value: boardFields.site, - validatedError: '', - verifiedError: '', col: 1, + label: 'Site', }, { key: KEYS.TOKEN, - value: boardFields.token, - validateRule: VALIDATOR.TOKEN, - validatedError: boardFields.token ? getValidatedError(KEYS.TOKEN, boardFields.token, VALIDATOR.TOKEN) : '', - verifiedError: '', col: 2, + label: 'Token', }, - ]); - - const getBoardInfo = (fields: Field[]) => { - const keys = ['type', 'boardId', 'email', 'site', 'token']; - return keys.reduce((board, key, index) => ({ ...board, [key]: fields[index].value }), {}); - }; + ]; - const handleUpdate = (fields: Field[]) => { - setFields(fields); - dispatch(updateBoardVerifyState(false)); - dispatch(updateBoard(getBoardInfo(fields))); + const persistReduxData = (shouldGetBoardConfig: boolean, boardInfo: IBoardConfigData & { projectKey?: string }) => { + dispatch(updateShouldGetBoardConfig(shouldGetBoardConfig)); + dispatch(updateBoard(boardInfo)); }; const resetFields = () => { - const newFields = fields.map((field) => - field.key === KEYS.BOARD - ? field - : { - ...field, - value: EMPTY_STRING, - validatedError: '', - verifiedError: '', - }, - ); - handleUpdate(newFields); - setIsShowAlert(false); - }; - - const getFieldsWithNoVerifiedError = (fields: Field[]) => - fields.map((field) => ({ - ...field, - verifiedError: '', - })); - - const updateField = (key: string, value: string) => { - const shouldClearVerifiedError = !!fields.find((field) => field.key === key)?.verifiedError; - const fieldsWithError = shouldClearVerifiedError ? getFieldsWithNoVerifiedError(fields) : fields; - const newFields = fieldsWithError.map((field) => - field.key === key - ? { - ...field, - value: value.trim(), - validatedError: getValidatedError(field.key, value.trim(), field.validateRule), - } - : field, - ); - handleUpdate(newFields); - }; - - const validateField = (key: string) => { - const newFields = fields.map((field) => - field.key === key - ? { - ...field, - validatedError: getValidatedError(field.key, field.value, field.validateRule), - } - : field, - ); - setFields(newFields); - }; - - const setVerifiedError = (keys: string[], messages: string[]) => { - setFields( - fields.map((field) => { - return keys.includes(field.key) - ? { - ...field, - validatedError: '', - verifiedError: messages[keys.findIndex((key) => key === field.key)], - } - : field; - }), - ); + reset(boardConfigOriginal); + persistReduxData(false, boardConfigOriginal); }; const verifyJira = async () => { setIsLoading(true); - const boardInfo = getBoardInfo(fields) as BoardRequestDTO; + dispatch(updateTreatFlagCardAsBlock(true)); + const boardInfo = getValues() as BoardRequestDTO; try { const res: { response: Record } = await boardClient.getVerifyBoard({ ...boardInfo, token: getJiraBoardToken(boardInfo.token, boardInfo.email), }); if (res?.response) { - setIsShowAlert(false); - setIsVerifyTimeOut(false); - dispatch(updateBoardVerifyState(true)); - dispatch(updateBoard({ ...boardInfo, projectKey: res.response.projectKey })); + persistReduxData(true, { ...boardInfo, projectKey: res.response.projectKey }); + reset(boardConfigOriginal, { keepValues: true }); } } catch (e) { if (isAppError(e)) { const { description, code } = e as IAppError; - setIsVerifyTimeOut(false); - setIsShowAlert(false); if (code === HttpStatusCode.Unauthorized) { - setVerifiedError( - [KEYS.EMAIL, KEYS.TOKEN], - [MESSAGE.VERIFY_MAIL_FAILED_ERROR, MESSAGE.VERIFY_TOKEN_FAILED_ERROR], - ); + setError(KEYS.EMAIL, { message: BOARD_CONFIG_ERROR_MESSAGE.email.verifyFailed }); + setError(KEYS.TOKEN, { message: BOARD_CONFIG_ERROR_MESSAGE.token.verifyFailed }); } else if (code === HttpStatusCode.NotFound && description === ERROR_INFO.SITE_NOT_FOUND) { - setVerifiedError([KEYS.SITE], [MESSAGE.VERIFY_SITE_FAILED_ERROR]); + setError(KEYS.SITE, { message: BOARD_CONFIG_ERROR_MESSAGE.site.verifyFailed }); } else if (code === HttpStatusCode.NotFound && description === ERROR_INFO.BOARD_NOT_FOUND) { - setVerifiedError([KEYS.BOARD_ID], [MESSAGE.VERIFY_BOARD_FAILED_ERROR]); + setError(KEYS.BOARD_ID, { message: BOARD_CONFIG_ERROR_MESSAGE.boardId.verifyFailed }); } else if (code === AXIOS_REQUEST_ERROR_CODE.TIMEOUT) { - setIsVerifyTimeOut(true); - setIsShowAlert(true); + setError(KEYS.TOKEN, { message: BOARD_CONFIG_ERROR_MESSAGE.token.timeout }); } else { - setVerifiedError([KEYS.TOKEN], [UNKNOWN_ERROR_TITLE]); + setError(KEYS.TOKEN, { message: UNKNOWN_ERROR_TITLE }); } } } @@ -226,12 +130,7 @@ export const useVerifyBoardEffect = (): useVerifyBoardStateInterface => { return { verifyJira, isLoading, - fields, - updateField, - validateField, + fields: originalFields, resetFields, - isVerifyTimeOut, - isShowAlert, - setIsShowAlert, }; }; diff --git a/frontend/src/hooks/useVerifyPipelineToolEffect.ts b/frontend/src/hooks/useVerifyPipelineToolEffect.ts index f006acc9d1..c07048512e 100644 --- a/frontend/src/hooks/useVerifyPipelineToolEffect.ts +++ b/frontend/src/hooks/useVerifyPipelineToolEffect.ts @@ -1,44 +1,70 @@ -import { updatePipelineToolVerifyState } from '@src/context/config/configSlice'; +import { PIPELINE_TOOL_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; +import { useDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; +import { initDeploymentFrequencySettings } from '@src/context/Metrics/metricsSlice'; +import { updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { pipelineToolClient } from '@src/clients/pipeline/PipelineToolClient'; +import { TPipelineToolFieldKeys } from '@src/containers/ConfigStep/Form/type'; import { IPipelineVerifyRequestDTO } from '@src/clients/pipeline/dto/request'; +import { IPipelineToolData } from '@src/containers/ConfigStep/Form/schema'; +import { updatePipelineTool } from '@src/context/config/configSlice'; import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources'; +import { useFormContext } from 'react-hook-form'; import { useAppDispatch } from '@src/hooks'; import { HttpStatusCode } from 'axios'; import { useState } from 'react'; +export enum FIELD_KEY { + TYPE = 0, + TOKEN = 1, +} +interface IField { + key: TPipelineToolFieldKeys; + label: string; +} + export const useVerifyPipelineToolEffect = () => { const [isLoading, setIsLoading] = useState(false); - const [verifiedError, setVerifiedError] = useState(''); const dispatch = useAppDispatch(); - const [isVerifyTimeOut, setIsVerifyTimeOut] = useState(false); - const [isShowAlert, setIsShowAlert] = useState(false); - const verifyPipelineTool = async (params: IPipelineVerifyRequestDTO): Promise => { + const { pipelineToolOriginal } = useDefaultValues(); + const fields: IField[] = [ + { key: 'type', label: 'Pipeline Tool' }, + { key: 'token', label: 'Token' }, + ]; + const { reset, setError, getValues } = useFormContext(); + const persistReduxData = (pipelineToolConfig: IPipelineToolData) => { + dispatch(updatePipelineTool(pipelineToolConfig)); + dispatch(updateShouldGetPipelineConfig(true)); + dispatch(initDeploymentFrequencySettings()); + }; + + const resetFields = () => { + reset(pipelineToolOriginal); + persistReduxData(pipelineToolOriginal); + }; + + const verifyPipelineTool = async (): Promise => { setIsLoading(true); - const response = await pipelineToolClient.verify(params); - setIsVerifyTimeOut(false); - setIsShowAlert(false); + const values = getValues() as IPipelineVerifyRequestDTO; + const response = await pipelineToolClient.verify(values); if (response.code === HttpStatusCode.NoContent) { - dispatch(updatePipelineToolVerifyState(true)); + reset(pipelineToolOriginal, { keepValues: true }); + persistReduxData(values); } else if (response.code === AXIOS_REQUEST_ERROR_CODE.TIMEOUT) { - setIsVerifyTimeOut(true); - setIsShowAlert(true); + setError(fields[FIELD_KEY.TOKEN].key, { message: PIPELINE_TOOL_ERROR_MESSAGE.token.timeout }); + } else if (response.code === HttpStatusCode.Unauthorized) { + setError(fields[FIELD_KEY.TOKEN].key, { message: PIPELINE_TOOL_ERROR_MESSAGE.token.unauthorized }); + } else if (response.code === HttpStatusCode.Forbidden) { + setError(fields[FIELD_KEY.TOKEN].key, { message: PIPELINE_TOOL_ERROR_MESSAGE.token.forbidden }); } else { - setVerifiedError(response.errorTitle); + setError(fields[FIELD_KEY.TOKEN].key, { message: response.errorTitle }); } setIsLoading(false); }; - const clearVerifiedError = () => { - if (verifiedError) setVerifiedError(''); - }; - return { + fields, verifyPipelineTool, isLoading, - verifiedError, - clearVerifiedError, - isVerifyTimeOut, - isShowAlert, - setIsShowAlert, + resetFields, }; }; diff --git a/frontend/src/hooks/useVerifySourceControlTokenEffect.ts b/frontend/src/hooks/useVerifySourceControlTokenEffect.ts index 53729da0db..87bbbd13f6 100644 --- a/frontend/src/hooks/useVerifySourceControlTokenEffect.ts +++ b/frontend/src/hooks/useVerifySourceControlTokenEffect.ts @@ -1,46 +1,67 @@ +import { initDeploymentFrequencySettings, updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; +import { SOURCE_CONTROL_ERROR_MESSAGE } from '@src/containers/ConfigStep/Form/literal'; import { SourceControlVerifyRequestDTO } from '@src/clients/sourceControl/dto/request'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; -import { updateSourceControlVerifyState } from '@src/context/config/configSlice'; +import { useDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; +import { TSourceControlFieldKeys } from '@src/containers/ConfigStep/Form/type'; +import { ISourceControlData } from '@src/containers/ConfigStep/Form/schema'; +import { updateSourceControl } from '@src/context/config/configSlice'; import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources'; import { useAppDispatch } from '@src/hooks/index'; -import { useCallback, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import { HttpStatusCode } from 'axios'; +import { useState } from 'react'; + +export enum FIELD_KEY { + TYPE = 0, + TOKEN = 1, +} + +interface IField { + key: TSourceControlFieldKeys; + label: string; +} export const useVerifySourceControlTokenEffect = () => { const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(false); - const [verifiedError, setVerifiedError] = useState(); - const [isVerifyTimeOut, setIsVerifyTimeOut] = useState(false); - const [isShowAlert, setIsShowAlert] = useState(false); - const verifyToken = async (params: SourceControlVerifyRequestDTO) => { + const fields: IField[] = [ + { key: 'type', label: 'Source Control' }, + { key: 'token', label: 'Token' }, + ]; + const { sourceControlOriginal } = useDefaultValues(); + const { reset, setError, getValues } = useFormContext(); + const persistReduxData = (sourceControlConfig: ISourceControlData) => { + dispatch(updateSourceControl(sourceControlConfig)); + dispatch(updateShouldGetPipelineConfig(true)); + dispatch(initDeploymentFrequencySettings()); + }; + const resetFields = () => { + reset(sourceControlOriginal); + }; + + const verifyToken = async () => { setIsLoading(true); - const response = await sourceControlClient.verifyToken(params); - setIsVerifyTimeOut(false); - setIsShowAlert(false); + const values = getValues() as SourceControlVerifyRequestDTO; + const response = await sourceControlClient.verifyToken(values); if (response.code === HttpStatusCode.NoContent) { - dispatch(updateSourceControlVerifyState(true)); + persistReduxData(values); + reset(sourceControlOriginal, { keepValues: true }); } else if (response.code === AXIOS_REQUEST_ERROR_CODE.TIMEOUT) { - setIsVerifyTimeOut(true); - setIsShowAlert(true); + setError(fields[FIELD_KEY.TOKEN].key, { message: SOURCE_CONTROL_ERROR_MESSAGE.token.timeout }); + } else if (response.code === HttpStatusCode.Unauthorized) { + setError(fields[FIELD_KEY.TOKEN].key, { message: SOURCE_CONTROL_ERROR_MESSAGE.token.unauthorized }); } else { - dispatch(updateSourceControlVerifyState(false)); - setVerifiedError(response.errorTitle); + setError(fields[FIELD_KEY.TOKEN].key, { message: response.errorTitle }); } setIsLoading(false); return response; }; - const clearVerifiedError = useCallback(() => { - setVerifiedError(''); - }, []); - return { verifyToken, isLoading, - verifiedError, - clearVerifiedError, - isVerifyTimeOut, - isShowAlert, - setIsShowAlert, + fields, + resetFields, }; };