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,
};
};