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,