diff --git a/frontend/__tests__/constants/fileConfig/fileConfig.test.ts b/frontend/__tests__/constants/fileConfig/fileConfig.test.ts index f53074678d..4d275e394d 100644 --- a/frontend/__tests__/constants/fileConfig/fileConfig.test.ts +++ b/frontend/__tests__/constants/fileConfig/fileConfig.test.ts @@ -3,6 +3,7 @@ import { BASIC_IMPORTED_OLD_CONFIG_FIXTURE, REGULAR_CALENDAR, CHINA_CALENDAR, + DEFAULT_REWORK_SETTINGS, } from '../../fixtures'; import { convertToNewFileConfig } from '@src/constants/fileConfig'; @@ -43,6 +44,7 @@ describe('#fileConfig', () => { organization: 'Thoughtworks-Heartbeat', }, ], + reworkTimesSettings: DEFAULT_REWORK_SETTINGS, }; it('should return original config when it is not old config', () => { diff --git a/frontend/__tests__/containers/ConfigStep/MetricsTypeCheckbox.test.tsx b/frontend/__tests__/containers/ConfigStep/MetricsTypeCheckbox.test.tsx index aa5d5ab7df..350ce798b1 100644 --- a/frontend/__tests__/containers/ConfigStep/MetricsTypeCheckbox.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/MetricsTypeCheckbox.test.tsx @@ -9,6 +9,7 @@ import { MEAN_TIME_TO_RECOVERY, REQUIRED_DATA, REQUIRED_DATA_LIST, + REWORK_TIMES, VELOCITY, } from '../../fixtures'; import { act, fireEvent, render, waitFor, within, screen } from '@testing-library/react'; @@ -70,7 +71,7 @@ describe('MetricsTypeCheckbox', () => { it('should show all selections when all option are select', async () => { const { getByRole, getByText } = setup(); - const displayedDataList = REQUIRED_DATA_LIST.slice(1, 8); + const displayedDataList = REQUIRED_DATA_LIST.slice(1); await act(async () => { await userEvent.click(getByRole('button', { name: REQUIRED_DATA })); }); @@ -85,7 +86,7 @@ describe('MetricsTypeCheckbox', () => { it('should show all selections when click velocity selection and then click all selection', async () => { const { getByRole, getByText } = setup(); - const displayedDataList = REQUIRED_DATA_LIST.slice(1, 8); + const displayedDataList = REQUIRED_DATA_LIST.slice(1); await act(async () => { await userEvent.click(getByRole('button', { name: REQUIRED_DATA })); @@ -113,6 +114,7 @@ describe('MetricsTypeCheckbox', () => { listBox.getByRole('option', { name: VELOCITY }), listBox.getByRole('option', { name: CYCLE_TIME }), listBox.getByRole('option', { name: CLASSIFICATION }), + listBox.getByRole('option', { name: REWORK_TIMES }), listBox.getByRole('option', { name: LEAD_TIME_FOR_CHANGES }), listBox.getByRole('option', { name: DEPLOYMENT_FREQUENCY }), listBox.getByRole('option', { name: CHANGE_FAILURE_RATE }), @@ -125,7 +127,7 @@ describe('MetricsTypeCheckbox', () => { it('should show some selections when click all option and then click velocity selection', async () => { const { getByRole, getByText } = setup(); - const displayedDataList = REQUIRED_DATA_LIST.slice(1, 7); + const displayedDataList = REQUIRED_DATA_LIST.slice(1, REQUIRED_DATA_LIST.length - 1); await act(async () => { await userEvent.click(getByRole('button', { name: REQUIRED_DATA })); diff --git a/frontend/__tests__/containers/MetricsStep/ReworkSettings/SingleSelection.test.tsx b/frontend/__tests__/containers/MetricsStep/ReworkSettings/SingleSelection.test.tsx new file mode 100644 index 0000000000..dd48502dae --- /dev/null +++ b/frontend/__tests__/containers/MetricsStep/ReworkSettings/SingleSelection.test.tsx @@ -0,0 +1,50 @@ +import { SingleSelection } from '@src/containers/MetricsStep/ReworkSettings/SingleSelection'; +import { act, render, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { LIST_OPEN } from '@test/fixtures'; + +describe('SingleSelection', () => { + const mockOptions = ['opton1', 'opton2', 'opton3']; + const mockLabel = 'mockLabel'; + const mockValue = 'mockOptions 1'; + const mockOnValueChange = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const setup = () => + render( + , + ); + + it('should trigger onValueChange callback when select value option', async () => { + const { getByText, getByRole, getAllByRole } = setup(); + + await waitFor(() => { + expect(getByText(mockLabel)).toBeInTheDocument(); + }); + + await act(async () => { + await userEvent.click(getAllByRole('button', { name: LIST_OPEN })[0]); + }); + + const stepsListBox = within(getByRole('listbox')); + await act(async () => { + await userEvent.click(stepsListBox.getByText(mockOptions[1])); + }); + + expect(mockOnValueChange).toHaveBeenCalledTimes(1); + }); + + it('should show no options when search the wrong keyword', async () => { + const { getAllByRole, getByText } = setup(); + const buttonElements = getAllByRole('button', { name: LIST_OPEN }); + + await act(async () => { + await userEvent.type(buttonElements[0], 'wrong keyword'); + }); + + expect(getByText('No options')).toBeInTheDocument(); + }); +}); diff --git a/frontend/__tests__/containers/MetricsStep/reworkSettings.test.tsx b/frontend/__tests__/containers/MetricsStep/reworkSettings.test.tsx new file mode 100644 index 0000000000..0598b3063a --- /dev/null +++ b/frontend/__tests__/containers/MetricsStep/reworkSettings.test.tsx @@ -0,0 +1,90 @@ +import { + ALL, + LIST_OPEN, + REWORK_EXCLUDE_WHICH_STATE, + REWORK_SETTINGS_TITLE, + REWORK_TO_WHICH_STATE, +} from '../../fixtures'; +import { act, render, screen, waitFor, within } from '@testing-library/react'; +import ReworkSettings from '@src/containers/MetricsStep/ReworkSettings'; +import { CYCLE_TIME_LIST } from '@src/constants/resources'; +import { setupStore } from '../../utils/setupStoreUtil'; +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import React from 'react'; + +const mockedUseAppDispatch = jest.fn(); +jest.mock('@src/hooks/useAppDispatch', () => ({ + useAppDispatch: () => mockedUseAppDispatch, +})); + +const store = setupStore(); + +describe('reworkSetting', () => { + const setup = () => + render( + + + , + ); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should show initial content', () => { + setup(); + + expect(screen.getByText(REWORK_SETTINGS_TITLE)).toBeInTheDocument(); + expect(screen.getByText(REWORK_TO_WHICH_STATE)).toBeInTheDocument(); + expect(screen.getByText(REWORK_EXCLUDE_WHICH_STATE)).toBeInTheDocument(); + }); + + it('should get correct rework setting when pick option', async () => { + const { getByRole, getAllByRole } = setup(); + await act(async () => { + await userEvent.click(getAllByRole('button', { name: LIST_OPEN })[0]); + }); + const stepsListBox = within(getByRole('listbox')); + await act(async () => { + await userEvent.click(stepsListBox.getByText('----')); + }); + await waitFor(async () => { + await expect( + (screen.getByTestId('rework-single-selection-rework-to-which-state').querySelector('input') as HTMLInputElement) + .value, + ).toBe('----'); + }); + }); + + it('should get correct value when pick all or other value', async () => { + const { getByRole, getAllByRole, queryByRole } = setup(); + await act(async () => { + await userEvent.click(getAllByRole('button', { name: LIST_OPEN })[1]); + }); + const stepsListBox = within(getByRole('listbox')); + await act(async () => { + await userEvent.click(stepsListBox.getByText(ALL)); + }); + await waitFor(async () => { + CYCLE_TIME_LIST.forEach((value) => { + expect(getByRole('button', { name: value })).toBeInTheDocument(); + }); + }); + + await act(async () => { + await userEvent.click(stepsListBox.getByText(ALL)); + }); + await waitFor(() => { + CYCLE_TIME_LIST.forEach((value) => { + expect(queryByRole('button', { name: value })).not.toBeInTheDocument(); + }); + }); + + await act(async () => { + await userEvent.click(stepsListBox.getByText(CYCLE_TIME_LIST[0])); + }); + await waitFor(async () => { + expect(getByRole('button', { name: CYCLE_TIME_LIST[0] })).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx b/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx index 86f72a7f44..d33ee64ccc 100644 --- a/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx +++ b/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx @@ -2,6 +2,7 @@ import { BASE_PAGE_ROUTE, BOARD_TYPES, CONFIRM_DIALOG_DESCRIPTION, + DEFAULT_REWORK_SETTINGS, MOCK_REPORT_URL, NEXT, PIPELINE_TOOL_TYPES, @@ -357,6 +358,7 @@ describe('MetricsStepper', () => { deployment: undefined, doneStatus: undefined, leadTime: undefined, + reworkTimesSettings: DEFAULT_REWORK_SETTINGS, }; setup(); @@ -388,6 +390,10 @@ describe('MetricsStepper', () => { deployment: undefined, doneStatus: undefined, leadTime: undefined, + reworkTimesSettings: { + rework2State: null, + excludeStates: [], + }, }; setup(); diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index 16710352b9..f3c4af1e1b 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -182,7 +182,7 @@ describe('Report Step', () => { }); it('should render the Lead Time For Change component with correct props', () => { - setup([REQUIRED_DATA_LIST[4]]); + setup([REQUIRED_DATA_LIST[5]]); expect(screen.getByText('60.79')).toBeInTheDocument(); expect(screen.getByText('39.03')).toBeInTheDocument(); @@ -190,20 +190,20 @@ describe('Report Step', () => { }); it('should render the Deployment frequency component with correct props', () => { - setup([REQUIRED_DATA_LIST[5]]); + setup([REQUIRED_DATA_LIST[6]]); expect(screen.getByText('0.40')).toBeInTheDocument(); }); it('should render the Change failure rate component with correct props', () => { - setup([REQUIRED_DATA_LIST[6]]); + setup([REQUIRED_DATA_LIST[7]]); expect(screen.getByText('0.00')).toBeInTheDocument(); expect(screen.getByText('% (0/6)')).toBeInTheDocument(); }); it('should render the Mean time to recovery component with correct props', () => { - setup([REQUIRED_DATA_LIST[7]]); + setup([REQUIRED_DATA_LIST[8]]); expect(screen.getByText('4.00')).toBeInTheDocument(); }); @@ -252,7 +252,7 @@ describe('Report Step', () => { jest.useRealTimers(); }); - it.each([[REQUIRED_DATA_LIST[1]], [REQUIRED_DATA_LIST[4]]])( + it.each([[REQUIRED_DATA_LIST[2]], [REQUIRED_DATA_LIST[5]]])( 'should render detail page when clicking show more button given metric %s', async (requiredData) => { setup([requiredData], MOCK_DATE_RANGE); @@ -266,7 +266,7 @@ describe('Report Step', () => { }, ); - it.each([[REQUIRED_DATA_LIST[1]], [REQUIRED_DATA_LIST[4]]])( + it.each([[REQUIRED_DATA_LIST[2]], [REQUIRED_DATA_LIST[5]]])( 'should return report page when clicking back button in Breadcrumb in detail page given metric %s', async (requiredData) => { setup([requiredData]); @@ -286,7 +286,7 @@ describe('Report Step', () => { }, ); - it.each([[REQUIRED_DATA_LIST[1]], [REQUIRED_DATA_LIST[4]]])( + it.each([[REQUIRED_DATA_LIST[2]], [REQUIRED_DATA_LIST[5]]])( 'should return report page when clicking previous button in detail page given metric %s', async (requiredData) => { setup([requiredData]); @@ -318,7 +318,7 @@ describe('Report Step', () => { expect(exportPipelineButton).not.toBeInTheDocument(); }); - it.each([[REQUIRED_DATA_LIST[4]], [REQUIRED_DATA_LIST[5]], [REQUIRED_DATA_LIST[6]], [REQUIRED_DATA_LIST[7]]])( + it.each([[REQUIRED_DATA_LIST[5]], [REQUIRED_DATA_LIST[6]], [REQUIRED_DATA_LIST[7]], [REQUIRED_DATA_LIST[8]]])( 'should show export pipeline button when selecting %s', (requiredData) => { setup([requiredData]); diff --git a/frontend/__tests__/context/metricsSlice.test.ts b/frontend/__tests__/context/metricsSlice.test.ts index 9b84a58f99..e5999e84a6 100644 --- a/frontend/__tests__/context/metricsSlice.test.ts +++ b/frontend/__tests__/context/metricsSlice.test.ts @@ -17,8 +17,13 @@ import saveMetricsSettingReducer, { updatePipelineStep, updateTreatFlagCardAsBlock, } from '@src/context/Metrics/metricsSlice'; +import { + CLASSIFICATION_WARNING_MESSAGE, + DEFAULT_REWORK_SETTINGS, + NO_RESULT_DASH, + PIPELINE_SETTING_TYPES, +} from '../fixtures'; import { ASSIGNEE_FILTER_TYPES, CYCLE_TIME_SETTINGS_TYPES, MESSAGE } from '@src/constants/resources'; -import { CLASSIFICATION_WARNING_MESSAGE, NO_RESULT_DASH, PIPELINE_SETTING_TYPES } from '../fixtures'; import { setupStore } from '../utils/setupStoreUtil'; import { store } from '@src/store'; @@ -50,6 +55,7 @@ const initState = { importedDeployment: [], importedLeadTime: [], importedAdvancedSettings: null, + reworkTimesSettings: DEFAULT_REWORK_SETTINGS, }, cycleTimeWarningMessage: null, classificationWarningMessage: null, @@ -111,6 +117,7 @@ describe('saveMetricsSetting reducer', () => { importedDeployment: [], importedPipelineCrews: [], importedAdvancedSettings: null, + reworkTimesSettings: DEFAULT_REWORK_SETTINGS, }); }); @@ -193,6 +200,7 @@ describe('saveMetricsSetting reducer', () => { storyPoint: '1', flag: '2', }, + reworkTimesSettings: DEFAULT_REWORK_SETTINGS, }; const savedMetricsSetting = saveMetricsSettingReducer( initState, @@ -212,6 +220,7 @@ describe('saveMetricsSetting reducer', () => { importedDeployment: mockMetricsImportedData.deployment, importedLeadTime: mockMetricsImportedData.leadTime, importedAdvancedSettings: mockMetricsImportedData.advancedSettings, + reworkTimesSettings: mockMetricsImportedData.reworkTimesSettings, }); }); diff --git a/frontend/__tests__/fixtures.ts b/frontend/__tests__/fixtures.ts index d3ea7d18ea..3b15835d7e 100644 --- a/frontend/__tests__/fixtures.ts +++ b/frontend/__tests__/fixtures.ts @@ -49,6 +49,7 @@ export const REQUIRED_DATA_LIST = [ 'Velocity', 'Cycle time', 'Classification', + 'Rework times', 'Lead time for changes', 'Deployment frequency', 'Change failure rate', @@ -58,6 +59,7 @@ export const ALL = 'All'; export const VELOCITY = 'Velocity'; export const CYCLE_TIME = 'Cycle time'; export const CLASSIFICATION = 'Classification'; +export const REWORK_TIMES = 'Rework times'; export const LEAD_TIME_FOR_CHANGES = 'Lead time for changes'; export const DEPLOYMENT_FREQUENCY = 'Deployment frequency'; export const CHANGE_FAILURE_RATE = 'Change failure rate'; @@ -247,6 +249,10 @@ export const IMPORTED_NEW_CONFIG_FIXTURE = { }, ], }, + reworkTimesSettings: { + rework2State: null, + excludeStates: [], + }, }; export const MOCK_EXPORT_CSV_REQUEST_PARAMS: CSVReportRequestDTO = { @@ -701,6 +707,10 @@ export const BASIC_IMPORTED_OLD_CONFIG_FIXTURE = { orgId: 'Thoughtworks-Heartbeat', }, ], + reworkTimesSettings: { + rework2State: null, + excludeStates: [], + }, }; export const ERROR_MESSAGE_TIME_DURATION = 4000; @@ -743,3 +753,12 @@ export const FAKE_TOKEN = 'fake-token'; export const FAKE_PIPELINE_TOKEN = 'bkua_mockTokenMockTokenMockTokenMockToken1234'; export const ADVANCED_SETTINGS_TITLE = 'Advanced settings'; + +export const REWORK_SETTINGS_TITLE = 'Rework times settings'; +export const REWORK_TO_WHICH_STATE = 'Rework to which state'; +export const REWORK_EXCLUDE_WHICH_STATE = 'Exclude which states (optional)'; + +export const DEFAULT_REWORK_SETTINGS = { + rework2State: null, + excludeStates: [], +}; diff --git a/frontend/e2e/fixtures/createNew/configStep.ts b/frontend/e2e/fixtures/createNew/configStep.ts index df5a5a8f21..f6eee9904a 100644 --- a/frontend/e2e/fixtures/createNew/configStep.ts +++ b/frontend/e2e/fixtures/createNew/configStep.ts @@ -9,6 +9,7 @@ export const config = { 'Velocity', 'Cycle time', 'Classification', + 'Rework times', 'Lead time for changes', 'Deployment frequency', 'Change failure rate', diff --git a/frontend/e2e/fixtures/createNew/metricsStep.ts b/frontend/e2e/fixtures/createNew/metricsStep.ts index d5382828ec..3baf2651f3 100644 --- a/frontend/e2e/fixtures/createNew/metricsStep.ts +++ b/frontend/e2e/fixtures/createNew/metricsStep.ts @@ -9,6 +9,7 @@ export const config = { 'Velocity', 'Cycle time', 'Classification', + 'Rework times', 'Lead time for changes', 'Deployment frequency', 'Change failure rate', @@ -25,6 +26,10 @@ export const config = { type: 'BuildKite', token: process.env.E2E_TOKEN_BUILD_KITE as string, }, + reworkTimesSettings: { + excludeStates: [], + rework2State: null, + }, sourceControl: { type: 'GitHub', token: process.env.E2E_TOKEN_GITHUB as string, @@ -126,6 +131,10 @@ export const modifiedConfig = { type: 'BuildKite', token: process.env.E2E_TOKEN_BUILD_KITE as string, }, + reworkTimesSettings: { + excludeStates: [], + rework2State: null, + }, sourceControl: { type: 'GitHub', token: process.env.E2E_TOKEN_GITHUB as string, diff --git a/frontend/e2e/fixtures/hb-e2e-for-importing-file.ts b/frontend/e2e/fixtures/hb-e2e-for-importing-file.ts index 6de33d29e0..e249c39a62 100644 --- a/frontend/e2e/fixtures/hb-e2e-for-importing-file.ts +++ b/frontend/e2e/fixtures/hb-e2e-for-importing-file.ts @@ -10,6 +10,7 @@ export const importProjectFromFile = { 'Velocity', 'Cycle time', 'Classification', + 'Rework times', 'Lead time for changes', 'Deployment frequency', 'Change failure rate', diff --git a/frontend/src/constants/fileConfig.ts b/frontend/src/constants/fileConfig.ts index 10c7acccf5..33da65d358 100644 --- a/frontend/src/constants/fileConfig.ts +++ b/frontend/src/constants/fileConfig.ts @@ -1,3 +1,4 @@ +import { IReworkConfig } from '@src/context/Metrics/metricsSlice'; import { CALENDAR } from '@src/constants/resources'; export interface OldFileConfig { @@ -35,6 +36,7 @@ export interface OldFileConfig { deployment?: OldConfigSetting[]; leadTime?: OldConfigSetting[]; pipelineCrews?: string[]; + reworkTimesSettings: IReworkConfig; } interface OldConfigSetting { @@ -87,7 +89,9 @@ export interface NewFileConfig { deployment?: NewConfigSetting[]; leadTime?: NewConfigSetting[]; pipelineCrews?: string[]; + reworkTimesSettings: IReworkConfig; } + export const convertToNewFileConfig = (fileConfig: OldFileConfig | NewFileConfig): NewFileConfig => { if ('considerHoliday' in fileConfig) { const { @@ -106,6 +110,7 @@ export const convertToNewFileConfig = (fileConfig: OldFileConfig | NewFileConfig classifications, deployment, pipelineCrews, + reworkTimesSettings, } = fileConfig; return { projectName, @@ -133,6 +138,7 @@ export const convertToNewFileConfig = (fileConfig: OldFileConfig | NewFileConfig pipelineCrews, cycleTime, doneStatus, + reworkTimesSettings, classification: classifications, deployment: deployment?.map((item, index) => ({ id: index, diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index e12a51bf50..cecea1b4a9 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -40,6 +40,7 @@ export enum REQUIRED_DATA { VELOCITY = 'Velocity', CYCLE_TIME = 'Cycle time', CLASSIFICATION = 'Classification', + REWORK_TIMES = 'Rework times', LEAD_TIME_FOR_CHANGES = 'Lead time for changes', DEPLOYMENT_FREQUENCY = 'Deployment frequency', CHANGE_FAILURE_RATE = 'Change failure rate', diff --git a/frontend/src/containers/MetricsStep/ReworkSettings/SingleSelection/index.tsx b/frontend/src/containers/MetricsStep/ReworkSettings/SingleSelection/index.tsx new file mode 100644 index 0000000000..2642d73661 --- /dev/null +++ b/frontend/src/containers/MetricsStep/ReworkSettings/SingleSelection/index.tsx @@ -0,0 +1,35 @@ +import { Autocomplete, TextField } from '@mui/material'; +import { Z_INDEX } from '@src/constants/commons'; +import { FormControlWrapper } from './style'; +import React from 'react'; + +interface Props { + options: string[]; + label: string; + value: string | null; + onValueChange: (value: string) => void; +} + +export const SingleSelection = ({ options, label, value, onValueChange }: Props) => { + const labelId = `rework-single-selection-${label.toLowerCase().replace(/\s/g, '-')}`; + + return ( + + onValueChange(newValue)} + renderInput={(params) => } + slotProps={{ + popper: { + sx: { + zIndex: Z_INDEX.DROPDOWN, + }, + }, + }} + /> + + ); +}; diff --git a/frontend/src/containers/MetricsStep/ReworkSettings/SingleSelection/style.tsx b/frontend/src/containers/MetricsStep/ReworkSettings/SingleSelection/style.tsx new file mode 100644 index 0000000000..0b0285b6f0 --- /dev/null +++ b/frontend/src/containers/MetricsStep/ReworkSettings/SingleSelection/style.tsx @@ -0,0 +1,6 @@ +import { styled } from '@mui/material/styles'; +import { FormControl } from '@mui/material'; + +export const FormControlWrapper = styled(FormControl)({ + height: '2.5rem', +}); diff --git a/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx b/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx new file mode 100644 index 0000000000..bc035c57db --- /dev/null +++ b/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx @@ -0,0 +1,62 @@ +import { selectReworkTimesSettings, updateReworkTimesSettings } from '@src/context/Metrics/metricsSlice'; +import { MetricsSettingTitle } from '@src/components/Common/MetricsSettingTitle'; +import { ReworkHeaderWrapper, ReworkSettingsWrapper, StyledLink } from './style'; +import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; +import MultiAutoComplete from '@src/components/Common/MultiAutoComplete'; +import { useAppDispatch, useAppSelector } from '@src/hooks'; +import { CYCLE_TIME_LIST } from '@src/constants/resources'; +import { SingleSelection } from './SingleSelection'; +import React from 'react'; + +const url = 'XXX'; + +function ReworkSettings() { + const reworkTimesSettings = useAppSelector(selectReworkTimesSettings); + const dispatch = useAppDispatch(); + + const isAllSelected = + CYCLE_TIME_LIST.length > 0 && reworkTimesSettings.excludeStates.length === CYCLE_TIME_LIST.length; + + const handleReworkSettingsChange = (_: React.SyntheticEvent, value: string[]) => { + let selectValue = value; + if (value[value.length - 1] === 'All') { + selectValue = reworkTimesSettings.excludeStates.length === CYCLE_TIME_LIST.length ? [] : CYCLE_TIME_LIST; + } + dispatch(updateReworkTimesSettings({ ...reworkTimesSettings, excludeStates: selectValue })); + }; + + return ( + <> + + + + + How to setup + + + + + dispatch(updateReworkTimesSettings({ ...reworkTimesSettings, rework2State: newValue })) + } + /> + + + + ); +} + +export default ReworkSettings; diff --git a/frontend/src/containers/MetricsStep/ReworkSettings/style.tsx b/frontend/src/containers/MetricsStep/ReworkSettings/style.tsx new file mode 100644 index 0000000000..b88e07b3ac --- /dev/null +++ b/frontend/src/containers/MetricsStep/ReworkSettings/style.tsx @@ -0,0 +1,29 @@ +import styled from '@emotion/styled'; +import { Link } from '@mui/material'; +import { theme } from '@src/theme'; + +export const StyledLink = styled(Link)({ + alignItems: 'center', + display: 'flex', + gap: '0.25rem', +}); + +export const ReworkHeaderWrapper = styled('div')({ + display: 'flex', + gap: '1rem', +}); + +export const ReworkSettingsWrapper = styled('div')` + position: relative; + width: 100%; + border: ${theme.main.cardBorder}; + box-shadow: none; + margin-bottom: 1rem; + line-height: '2rem'; + boarder-radius: 0.25rem; + padding: 2.5%; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 1rem; +`; diff --git a/frontend/src/containers/MetricsStep/index.tsx b/frontend/src/containers/MetricsStep/index.tsx index e767306899..01433b01cf 100644 --- a/frontend/src/containers/MetricsStep/index.tsx +++ b/frontend/src/containers/MetricsStep/index.tsx @@ -34,6 +34,7 @@ import { useAppSelector, useAppDispatch } from '@src/hooks'; import { Crews } from '@src/containers/MetricsStep/Crews'; import { useCallback, useLayoutEffect } from 'react'; import { Loading } from '@src/components/Loading'; +import ReworkSettings from './ReworkSettings'; import { Advance } from './Advance/Advance'; import isEmpty from 'lodash/isEmpty'; import merge from 'lodash/merge'; @@ -52,7 +53,8 @@ const MetricsStep = () => { const isShowCrewsAndRealDone = requiredData.includes(REQUIRED_DATA.VELOCITY) || requiredData.includes(REQUIRED_DATA.CYCLE_TIME) || - requiredData.includes(REQUIRED_DATA.CLASSIFICATION); + requiredData.includes(REQUIRED_DATA.CLASSIFICATION) || + requiredData.includes(REQUIRED_DATA.REWORK_TIMES); const isShowRealDone = cycleTimeSettingsType === CYCLE_TIME_SETTINGS_TYPES.BY_COLUMN && cycleTimeSettings.filter((e) => e.value === DONE).length > 1; @@ -115,6 +117,7 @@ const MetricsStep = () => { /> )} + {requiredData.includes(REQUIRED_DATA.REWORK_TIMES) && } ) : ( { advancedSettings: importedData.importedAdvancedSettings, deployment: deploymentFrequencySettings, leadTime: leadTimeForChanges, + reworkTimesSettings: importedData.reworkTimesSettings, }; const jsonData = activeStep === METRICS_STEPS.CONFIG ? configData : { ...configData, ...metricsData }; exportToJsonFile('config', jsonData); diff --git a/frontend/src/context/Metrics/metricsSlice.ts b/frontend/src/context/Metrics/metricsSlice.ts index 4c854cd97f..cb7586a33a 100644 --- a/frontend/src/context/Metrics/metricsSlice.ts +++ b/frontend/src/context/Metrics/metricsSlice.ts @@ -20,6 +20,11 @@ export interface IPipelineConfig { branches: string[]; } +export interface IReworkConfig { + rework2State: string | null; + excludeStates: string[]; +} + export interface IPipelineWarningMessageConfig { id: number | null; organization: string | null; @@ -59,6 +64,7 @@ export interface savedMetricsSettingState { importedClassification: string[]; importedDeployment: IPipelineConfig[]; importedAdvancedSettings: { storyPoint: string; flag: string } | null; + reworkTimesSettings: IReworkConfig; }; cycleTimeWarningMessage: string | null; classificationWarningMessage: string | null; @@ -92,6 +98,10 @@ const initialState: savedMetricsSettingState = { importedClassification: [], importedDeployment: [], importedAdvancedSettings: null, + reworkTimesSettings: { + rework2State: null, + excludeStates: [], + }, }, cycleTimeWarningMessage: null, classificationWarningMessage: null, @@ -264,6 +274,7 @@ export const metricsSlice = createSlice({ leadTime, assigneeFilter, pipelineCrews, + reworkTimesSettings, } = action.payload; state.importedData.importedCrews = crews || state.importedData.importedCrews; state.importedData.importedPipelineCrews = pipelineCrews || state.importedData.importedPipelineCrews; @@ -276,6 +287,7 @@ export const metricsSlice = createSlice({ state.importedData.importedClassification = classification || state.importedData.importedClassification; state.importedData.importedDeployment = deployment || leadTime || state.importedData.importedDeployment; state.importedData.importedAdvancedSettings = advancedSettings || state.importedData.importedAdvancedSettings; + state.importedData.reworkTimesSettings = reworkTimesSettings || state.importedData.reworkTimesSettings; }, updateMetricsState: (state, action) => { @@ -486,6 +498,10 @@ export const metricsSlice = createSlice({ updateAdvancedSettings: (state, action) => { state.importedData.importedAdvancedSettings = action.payload; }, + + updateReworkTimesSettings: (state, action) => { + state.importedData.reworkTimesSettings = action.payload; + }, }, }); @@ -510,12 +526,14 @@ export const { updateAdvancedSettings, updateShouldGetBoardConfig, updateShouldGetPipelineConfig, + updateReworkTimesSettings, } = metricsSlice.actions; export const selectShouldGetBoardConfig = (state: RootState) => state.metrics.shouldGetBoardConfig; export const selectShouldGetPipelineConfig = (state: RootState) => state.metrics.shouldGetPipeLineConfig; export const selectDeploymentFrequencySettings = (state: RootState) => state.metrics.deploymentFrequencySettings; +export const selectReworkTimesSettings = (state: RootState) => state.metrics.importedData.reworkTimesSettings; export const selectCycleTimeSettings = (state: RootState) => state.metrics.cycleTimeSettings; export const selectMetricsContent = (state: RootState) => state.metrics; diff --git a/frontend/src/context/config/configSlice.ts b/frontend/src/context/config/configSlice.ts index 6fb698cf33..1e3602e0cd 100644 --- a/frontend/src/context/config/configSlice.ts +++ b/frontend/src/context/config/configSlice.ts @@ -51,10 +51,11 @@ const getMetricsInfo = (metrics: string[]) => { DEPLOYMENT_FREQUENCY, CHANGE_FAILURE_RATE, MEAN_TIME_TO_RECOVERY, + REWORK_TIMES, } = REQUIRED_DATA; return { metrics: metrics.filter((metric) => (Object.values(REQUIRED_DATA) as string[]).includes(metric)), - shouldBoardShow: [VELOCITY, CYCLE_TIME, CLASSIFICATION].some((metric) => metrics.includes(metric)), + shouldBoardShow: [VELOCITY, CYCLE_TIME, CLASSIFICATION, REWORK_TIMES].some((metric) => metrics.includes(metric)), shouldPipelineToolShow: [ LEAD_TIME_FOR_CHANGES, DEPLOYMENT_FREQUENCY,