diff --git a/frontend/__tests__/constants/fileConfig/fileConfig.test.ts b/frontend/__tests__/constants/fileConfig/fileConfig.test.ts index 4d275e394d..4cc0dc54c3 100644 --- a/frontend/__tests__/constants/fileConfig/fileConfig.test.ts +++ b/frontend/__tests__/constants/fileConfig/fileConfig.test.ts @@ -76,4 +76,34 @@ describe('#fileConfig', () => { }), ).toEqual(expected); }); + + it('should get default rework value when rework2State and excludeStates are all invalid', () => { + const expected = { + ...BASIC_NEW_CONFIG, + calendarType: CHINA_CALENDAR, + reworkTimesSettings: { rework2State: null, excludeStates: [] }, + }; + expect( + convertToNewFileConfig({ + ...BASIC_IMPORTED_OLD_CONFIG_FIXTURE, + considerHoliday: true, + reworkTimesSettings: { rework2State: 'test', excludeStates: ['In Dev'] }, + }), + ).toEqual(expected); + }); + + it('should filter invalid rework value when excludeStates field is invalid', () => { + const expected = { + ...BASIC_NEW_CONFIG, + calendarType: CHINA_CALENDAR, + reworkTimesSettings: { rework2State: 'In Dev', excludeStates: ['Review'] }, + }; + expect( + convertToNewFileConfig({ + ...BASIC_IMPORTED_OLD_CONFIG_FIXTURE, + considerHoliday: true, + reworkTimesSettings: { rework2State: 'In Dev', excludeStates: ['Review', 'test'] }, + }), + ).toEqual(expected); + }); }); diff --git a/frontend/__tests__/containers/MetricsStep/CycleTime.test.tsx b/frontend/__tests__/containers/MetricsStep/CycleTime.test.tsx index f46a9d10ec..52f70410c1 100644 --- a/frontend/__tests__/containers/MetricsStep/CycleTime.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/CycleTime.test.tsx @@ -269,7 +269,7 @@ describe('CycleTime', () => { const inputElements = screen.getAllByRole('combobox'); const selectedInputValue = inputElements.map((option) => option.getAttribute('value'))[0]; - expect(selectedInputValue).toBe('Review'); + expect(selectedInputValue).toBe('Testing'); await waitFor(() => expect(mockedUseAppDispatch).toHaveBeenCalledWith(saveDoneColumn([]))); }); }); @@ -402,7 +402,7 @@ describe('CycleTime', () => { const inputElements = screen.getAllByRole('combobox'); const selectedInputValue = inputElements.map((option) => option.getAttribute('value'))[0]; - expect(selectedInputValue).toBe('Review'); + expect(selectedInputValue).toBe('Testing'); expect(mockedUseAppDispatch).not.toHaveBeenCalledWith(saveDoneColumn([])); }); }); diff --git a/frontend/__tests__/containers/MetricsStep/reworkSettings.test.tsx b/frontend/__tests__/containers/MetricsStep/reworkSettings.test.tsx index 320383f7dd..83e836fdfc 100644 --- a/frontend/__tests__/containers/MetricsStep/reworkSettings.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/reworkSettings.test.tsx @@ -7,6 +7,7 @@ import { } from '../../fixtures'; import { METRICS_CONSTANTS, REWORK_TIME_LIST } from '@src/constants/resources'; import { act, render, screen, waitFor, within } from '@testing-library/react'; +import { updateCycleTimeSettings } from '@src/context/Metrics/metricsSlice'; import ReworkSettings from '@src/containers/MetricsStep/ReworkSettings'; import { setupStore } from '../../utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; @@ -19,6 +20,29 @@ jest.mock('@src/hooks/useAppDispatch', () => ({ })); const store = setupStore(); +const mockCycleTimeSettings1 = [ + { + column: 'TODO', + status: 'TODO', + value: 'To do', + }, + { + column: 'Doing', + status: 'DOING', + value: 'In Dev', + }, + { + column: 'Blocked', + status: 'BLOCKED', + value: 'Block', + }, + { + column: 'Done', + status: 'DONE', + value: 'Done', + }, +]; +const mockCycleTimeSettings2 = mockCycleTimeSettings1.filter((item) => item.value !== 'Done'); describe('reworkSetting', () => { const setup = () => @@ -27,6 +51,11 @@ describe('reworkSetting', () => { , ); + + beforeEach(() => { + store.dispatch(updateCycleTimeSettings(mockCycleTimeSettings1)); + }); + afterEach(() => { jest.clearAllMocks(); }); @@ -100,7 +129,7 @@ describe('reworkSetting', () => { await userEvent.click(stepsListBox2.getByText(ALL)); }); await waitFor(async () => { - REWORK_TIME_LIST.slice(1).forEach((value) => { + [...new Set(mockCycleTimeSettings1.map((item) => item.value))].slice(1).forEach((value) => { expect(getByRole('button', { name: value })).toBeInTheDocument(); }); }); @@ -109,16 +138,41 @@ describe('reworkSetting', () => { await userEvent.click(stepsListBox2.getByText(ALL)); }); await waitFor(() => { - REWORK_TIME_LIST.forEach((value) => { + [...new Set(mockCycleTimeSettings1.map((item) => item.value))].slice(1).forEach((value) => { expect(queryByRole('button', { name: value })).not.toBeInTheDocument(); }); }); await act(async () => { - await userEvent.click(stepsListBox2.getByText(REWORK_TIME_LIST[1])); + await userEvent.click(stepsListBox2.getByText(REWORK_TIME_LIST[3])); }); await waitFor(async () => { - expect(getByRole('button', { name: REWORK_TIME_LIST[1] })).toBeInTheDocument(); + expect(getByRole('button', { name: REWORK_TIME_LIST[3] })).toBeInTheDocument(); + }); + }); + + it('should get correct value when board mappings have not done value', async () => { + store.dispatch(updateCycleTimeSettings(mockCycleTimeSettings2)); + const { getByRole, getAllByRole } = setup(); + await act(async () => { + await userEvent.click(getAllByRole('button', { name: LIST_OPEN })[0]); + }); + const stepsListBox1 = within(getByRole('listbox')); + await act(async () => { + await userEvent.click(stepsListBox1.getByText(METRICS_CONSTANTS.todoValue)); + }); + await act(async () => { + await userEvent.click(getAllByRole('button', { name: LIST_OPEN })[1]); + }); + const stepsListBox2 = within(getByRole('listbox')); + + await act(async () => { + await userEvent.click(stepsListBox2.getByText(ALL)); + }); + await waitFor(async () => { + [...new Set(mockCycleTimeSettings2.map((item) => item.value))].slice(1).forEach((value) => { + expect(getByRole('button', { name: value })).toBeInTheDocument(); + }); }); }); }); diff --git a/frontend/__tests__/hooks/useVerifyPipelineToolEffect.test.tsx b/frontend/__tests__/hooks/useVerifyPipelineToolEffect.test.tsx index c75bdab78f..f8e8bd4da1 100644 --- a/frontend/__tests__/hooks/useVerifyPipelineToolEffect.test.tsx +++ b/frontend/__tests__/hooks/useVerifyPipelineToolEffect.test.tsx @@ -17,6 +17,10 @@ jest.mock('react-redux', () => ({ 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, + }); + const { result } = renderHook(() => useVerifyPipelineToolEffect()); act(() => { diff --git a/frontend/src/constants/fileConfig.ts b/frontend/src/constants/fileConfig.ts index 33da65d358..12c440e192 100644 --- a/frontend/src/constants/fileConfig.ts +++ b/frontend/src/constants/fileConfig.ts @@ -1,5 +1,5 @@ +import { CALENDAR, REWORK_TIME_LIST } from '@src/constants/resources'; import { IReworkConfig } from '@src/context/Metrics/metricsSlice'; -import { CALENDAR } from '@src/constants/resources'; export interface OldFileConfig { projectName: string; @@ -36,7 +36,7 @@ export interface OldFileConfig { deployment?: OldConfigSetting[]; leadTime?: OldConfigSetting[]; pipelineCrews?: string[]; - reworkTimesSettings: IReworkConfig; + reworkTimesSettings?: IReworkConfig; } interface OldConfigSetting { @@ -89,9 +89,23 @@ export interface NewFileConfig { deployment?: NewConfigSetting[]; leadTime?: NewConfigSetting[]; pipelineCrews?: string[]; - reworkTimesSettings: IReworkConfig; + reworkTimesSettings?: IReworkConfig; } +const filterExcludeReworkStatus = (reworkTimesSettings: IReworkConfig | undefined) => { + if (!reworkTimesSettings) return; + const rework2State = REWORK_TIME_LIST.includes(reworkTimesSettings?.rework2State as string) + ? reworkTimesSettings.rework2State + : null; + const excludeStates = reworkTimesSettings?.excludeStates.filter((value) => { + return REWORK_TIME_LIST.includes(value); + }); + return { + rework2State, + excludeStates: rework2State ? excludeStates : [], + }; +}; + export const convertToNewFileConfig = (fileConfig: OldFileConfig | NewFileConfig): NewFileConfig => { if ('considerHoliday' in fileConfig) { const { @@ -138,7 +152,7 @@ export const convertToNewFileConfig = (fileConfig: OldFileConfig | NewFileConfig pipelineCrews, cycleTime, doneStatus, - reworkTimesSettings, + reworkTimesSettings: filterExcludeReworkStatus(reworkTimesSettings), classification: classifications, deployment: deployment?.map((item, index) => ({ id: index, @@ -149,5 +163,5 @@ export const convertToNewFileConfig = (fileConfig: OldFileConfig | NewFileConfig })), }; } - return fileConfig; + return { ...fileConfig, reworkTimesSettings: filterExcludeReworkStatus(fileConfig.reworkTimesSettings) }; }; diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index 2d5c3bf098..4b902de48e 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -161,9 +161,9 @@ export const CYCLE_TIME_LIST = [ METRICS_CONSTANTS.analysisValue, METRICS_CONSTANTS.inDevValue, METRICS_CONSTANTS.blockValue, + METRICS_CONSTANTS.reviewValue, METRICS_CONSTANTS.waitingValue, METRICS_CONSTANTS.testingValue, - METRICS_CONSTANTS.reviewValue, METRICS_CONSTANTS.doneValue, ]; diff --git a/frontend/src/containers/MetricsStep/Advance/Advance.tsx b/frontend/src/containers/MetricsStep/Advance/Advance.tsx index 14681bf1b1..aef862eeda 100644 --- a/frontend/src/containers/MetricsStep/Advance/Advance.tsx +++ b/frontend/src/containers/MetricsStep/Advance/Advance.tsx @@ -1,6 +1,6 @@ import { selectAdvancedSettings, updateAdvancedSettings } from '@src/context/Metrics/metricsSlice'; +import { AdvancedContainer, AdvancedForm, AdvancedTitleContainer, AdvancedWrapper } from './style'; import { ItemCheckbox, TitleAndTooltipContainer, TooltipContainer } from '../CycleTime/style'; -import { AdvancedContainer, AdvancedForm, AdvancedTitleContainer } from './style'; import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; import { StyledLink } from '@src/containers/MetricsStep/style'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; @@ -82,21 +82,23 @@ export const Advance = () => { {open && ( <> - - {fields.map(({ key, col, value }, index) => ( - updateField(key, e.target.value)} - placeholder={'Customized filed key'} - /> - ))} - + + + {fields.map(({ key, col, value }, index) => ( + updateField(key, e.target.value)} + placeholder={'Customized filed key'} + /> + ))} + + )} diff --git a/frontend/src/containers/MetricsStep/Advance/style.tsx b/frontend/src/containers/MetricsStep/Advance/style.tsx index 7f6c383fcd..5a834de518 100644 --- a/frontend/src/containers/MetricsStep/Advance/style.tsx +++ b/frontend/src/containers/MetricsStep/Advance/style.tsx @@ -10,17 +10,31 @@ export const AdvancedTitleContainer = styled.div({ export const AdvancedContainer = styled('div')({ display: 'flex', - margin: '1rem 0 0.375rem', + margin: '1rem 0 1.25rem', }); export const AdvancedForm = styled('form')({ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1rem', - marginTop: '1rem', marginBottom: '1rem', width: '100%', [theme.breakpoints.down('md')]: { gridTemplateColumns: '1fr', }, }); + +export const AdvancedWrapper = 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/CycleTime/Table/index.tsx b/frontend/src/containers/MetricsStep/CycleTime/Table/index.tsx index 912b314fe8..bc91ed6375 100644 --- a/frontend/src/containers/MetricsStep/CycleTime/Table/index.tsx +++ b/frontend/src/containers/MetricsStep/CycleTime/Table/index.tsx @@ -10,6 +10,7 @@ import { saveDoneColumn, selectMetricsContent, setCycleTimeSettingsType, + updateReworkTimesSettings, } from '@src/context/Metrics/metricsSlice'; import { StyledRadioGroup, @@ -56,6 +57,7 @@ const CycleTimeTable = () => { ); isColumnAsKey && resetRealDoneColumn(name, value); dispatch(updateCycleTimeSettings(newCycleTimeSettings)); + dispatch(updateReworkTimesSettings({ excludeStates: [], rework2State: null })); }, [cycleTimeSettings, dispatch, isColumnAsKey, resetRealDoneColumn], ); diff --git a/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx b/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx index 6708b10316..78f8f37883 100644 --- a/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx +++ b/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx @@ -1,4 +1,8 @@ -import { selectReworkTimesSettings, updateReworkTimesSettings } from '@src/context/Metrics/metricsSlice'; +import { + selectReworkTimesSettings, + updateReworkTimesSettings, + selectCycleTimeSettings, +} from '@src/context/Metrics/metricsSlice'; import { ReworkDialog } from '@src/containers/MetricsStep/ReworkSettings/ReworkDialog'; import { MetricsSettingTitle } from '@src/components/Common/MetricsSettingTitle'; import { METRICS_CONSTANTS, REWORK_TIME_LIST } from '@src/constants/resources'; @@ -8,26 +12,39 @@ import { ReworkHeaderWrapper, ReworkSettingsWrapper } from './style'; import { StyledLink } from '@src/containers/MetricsStep/style'; import { useAppDispatch, useAppSelector } from '@src/hooks'; import { SingleSelection } from './SingleSelection'; -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; function ReworkSettings() { const [isShowDialog, setIsShowDialog] = useState(false); const reworkTimesSettings = useAppSelector(selectReworkTimesSettings); + const cycleTimeSettings = useAppSelector(selectCycleTimeSettings); const dispatch = useAppDispatch(); + const boardingMappingStatus = useMemo(() => { + return [...new Set(cycleTimeSettings.map((item) => item.value))]; + }, [cycleTimeSettings]); - const MultiOptions = reworkTimesSettings.rework2State + const boardingMappingHasDoneStatus = boardingMappingStatus.includes(METRICS_CONSTANTS.doneValue); + const allStateIsEmpty = boardingMappingStatus.every((value) => value === METRICS_CONSTANTS.cycleTimeEmptyStr); + const onlyDoneStateSelected = boardingMappingHasDoneStatus && boardingMappingStatus.length === 2; + const singleOptions = boardingMappingStatus + .filter((item) => item !== METRICS_CONSTANTS.doneValue && item !== METRICS_CONSTANTS.cycleTimeEmptyStr) + .sort((a, b) => { + return REWORK_TIME_LIST.indexOf(a) - REWORK_TIME_LIST.indexOf(b); + }); + + const multiOptions = reworkTimesSettings.rework2State ? [ - ...REWORK_TIME_LIST.slice(REWORK_TIME_LIST.indexOf(reworkTimesSettings.rework2State) + 1), - METRICS_CONSTANTS.doneValue, + ...singleOptions.slice(singleOptions.indexOf(reworkTimesSettings.rework2State as string) + 1), + ...(boardingMappingHasDoneStatus ? [METRICS_CONSTANTS.doneValue] : []), ] : []; - const isAllSelected = MultiOptions.length > 0 && reworkTimesSettings.excludeStates.length === MultiOptions.length; + const isAllSelected = multiOptions.length > 0 && reworkTimesSettings.excludeStates.length === multiOptions.length; const handleReworkSettingsChange = (_: React.SyntheticEvent, value: string[]) => { let selectValue = value; if (value[value.length - 1] === 'All') { - selectValue = reworkTimesSettings.excludeStates.length === MultiOptions.length ? [] : MultiOptions; + selectValue = reworkTimesSettings.excludeStates.length === multiOptions.length ? [] : multiOptions; } dispatch(updateReworkTimesSettings({ ...reworkTimesSettings, excludeStates: selectValue })); }; @@ -50,28 +67,32 @@ function ReworkSettings() { How to setup - - - dispatch(updateReworkTimesSettings({ excludeStates: [], rework2State: newValue })) - } - /> - - + {allStateIsEmpty || onlyDoneStateSelected ? ( + <> + ) : ( + + + dispatch(updateReworkTimesSettings({ excludeStates: [], rework2State: newValue })) + } + /> + + + )} ); } diff --git a/frontend/src/containers/MetricsStepper/index.tsx b/frontend/src/containers/MetricsStepper/index.tsx index b6e4397786..8ba975f132 100644 --- a/frontend/src/containers/MetricsStepper/index.tsx +++ b/frontend/src/containers/MetricsStepper/index.tsx @@ -78,6 +78,8 @@ const MetricsStepper = () => { requiredData.includes(REQUIRED_DATA.CLASSIFICATION) || requiredData.includes(REQUIRED_DATA.VELOCITY); const isCycleTimeSettingsVerified = cycleTimeSettings.some((e) => e.value === DONE); + const boardingMappingStatus = [...new Set(cycleTimeSettings.map((item) => item.value))]; + const onlyDoneStateSelected = isCycleTimeSettingsVerified && boardingMappingStatus.length === 2; const isShowClassificationSetting = requiredData.includes(REQUIRED_DATA.CLASSIFICATION); const isShowReworkSettings = requiredData.includes(REQUIRED_DATA.REWORK_TIMES); const isClassificationSettingVerified = metricsConfig.targetFields.some((item) => item.flag); @@ -135,7 +137,7 @@ const MetricsStepper = () => { { isShow: isShowDeploymentFrequency, isValid: isDeploymentFrequencyValid }, { isShow: isShowCycleTimeSettings, isValid: isCycleTimeSettingsVerified }, { isShow: isShowClassificationSetting, isValid: isClassificationSettingVerified }, - { isShow: isShowReworkSettings, isValid: isRework2StateSelected }, + { isShow: isShowReworkSettings, isValid: isRework2StateSelected || onlyDoneStateSelected }, ]; const activeNextButtonValidityOptions = nextButtonValidityOptions.filter(({ isShow }) => isShow); activeNextButtonValidityOptions.every(({ isValid }) => isValid) @@ -165,6 +167,7 @@ const MetricsStepper = () => { isClassificationSettingVerified, isRework2StateSelected, isShowReworkSettings, + onlyDoneStateSelected, ]); const filterMetricsConfig = (metricsConfig: savedMetricsSettingState) => {