From 9d87a769a8fc50607f49d9e0027cf36cbca70564 Mon Sep 17 00:00:00 2001 From: Leiqiuhong <141199516+Leiqiuhong@users.noreply.github.com> Date: Sun, 7 Apr 2024 12:48:39 +0800 Subject: [PATCH] Adm 897 [frontend]: board configuration need to retain modified data when user change time range or reverify (#1335) * ADM-897 feat: add logic for crew users when reload * ADM-897 feat: add logic for classification when reload * ADM-897 feat: reset rework time setting when mapping modified * ADM-897 feat: add boarding mapping logic when reload * ADM-897 test: add unit test for util methods * ADM-897 test: add unit test for metrics slice * ADM-897 test: add unit test for util method * ADM-897 fix: filter empty option * ADM-897 fix: fix eslint * ADM-897 fix: fix eslint * ADM-897 fix: fix merge --- .../ConfigStep/DateRangePicker.test.tsx | 3 - .../__tests__/context/metricsSlice.test.ts | 155 +++++++++++++++--- frontend/__tests__/utils/Util.test.tsx | 141 +++++++++++++++- .../DateRangePicker/DateRangePicker.tsx | 2 - .../MetricsStep/Classification/index.tsx | 6 +- .../containers/MetricsStep/Crews/index.tsx | 15 +- .../MetricsStep/ReworkSettings/index.tsx | 20 +-- frontend/src/containers/MetricsStep/index.tsx | 2 + .../src/containers/MetricsStepper/index.tsx | 18 +- frontend/src/context/Metrics/metricsSlice.ts | 138 +++++++++++++--- frontend/src/utils/util.ts | 24 ++- 11 files changed, 430 insertions(+), 94 deletions(-) diff --git a/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx b/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx index 86c9b603db..1cc9dd0281 100644 --- a/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx @@ -1,6 +1,5 @@ import { initDeploymentFrequencySettings, - saveUsers, updateShouldGetBoardConfig, updateShouldGetPipelineConfig, } from '@src/context/Metrics/metricsSlice'; @@ -133,7 +132,6 @@ describe('DateRangePickerSection', () => { expect(updateShouldGetBoardConfig).toHaveBeenCalledWith(true); expect(updateShouldGetPipelineConfig).toHaveBeenCalledWith(true); expect(initDeploymentFrequencySettings).toHaveBeenCalled(); - expect(saveUsers).toHaveBeenCalledWith([]); }); it('should dispatch update configuration when change endDate', async () => { @@ -145,7 +143,6 @@ describe('DateRangePickerSection', () => { expect(updateShouldGetBoardConfig).toHaveBeenCalledWith(true); expect(updateShouldGetPipelineConfig).toHaveBeenCalledWith(true); expect(initDeploymentFrequencySettings).toHaveBeenCalled(); - expect(saveUsers).toHaveBeenCalledWith([]); }); }); diff --git a/frontend/__tests__/context/metricsSlice.test.ts b/frontend/__tests__/context/metricsSlice.test.ts index 60e851fcbd..8f9d4a80f9 100644 --- a/frontend/__tests__/context/metricsSlice.test.ts +++ b/frontend/__tests__/context/metricsSlice.test.ts @@ -2,39 +2,39 @@ import saveMetricsSettingReducer, { addADeploymentFrequencySetting, deleteADeploymentFrequencySetting, initDeploymentFrequencySettings, - updateCycleTimeSettings, + resetMetricData, saveDoneColumn, + savePipelineCrews, saveTargetFields, saveUsers, + selectAdvancedSettings, + selectAssigneeFilter, + selectClassificationWarningMessage, + selectCycleTimeSettings, + selectCycleTimeWarningMessage, selectDeploymentFrequencySettings, + selectMetricsContent, selectOrganizationWarningMessage, selectPipelineNameWarningMessage, + selectRealDoneWarningMessage, + selectReworkTimesSettings, + selectShouldGetBoardConfig, + selectShouldGetPipelineConfig, selectStepWarningMessage, + selectTreatFlagCardAsBlock, + setCycleTimeSettingsType, + updateAdvancedSettings, + updateAssigneeFilter, + updateCycleTimeSettings, updateDeploymentFrequencySettings, updateMetricsImportedData, updateMetricsState, updatePipelineSettings, updatePipelineStep, - updateTreatFlagCardAsBlock, - updateAssigneeFilter, - resetMetricData, - savePipelineCrews, - setCycleTimeSettingsType, - updateShouldGetPipelineConfig, - updateShouldGetBoardConfig, - updateAdvancedSettings, updateReworkTimesSettings, - selectShouldGetBoardConfig, - selectShouldGetPipelineConfig, - selectAdvancedSettings, - selectAssigneeFilter, - selectClassificationWarningMessage, - selectCycleTimeSettings, - selectCycleTimeWarningMessage, - selectMetricsContent, - selectRealDoneWarningMessage, - selectReworkTimesSettings, - selectTreatFlagCardAsBlock, + updateShouldGetBoardConfig, + updateShouldGetPipelineConfig, + updateTreatFlagCardAsBlock, } from '@src/context/Metrics/metricsSlice'; import { CLASSIFICATION_WARNING_MESSAGE, @@ -81,6 +81,7 @@ const initState = { realDoneWarningMessage: null, deploymentWarningMessage: [], leadTimeWarningMessage: [], + firstTimeRoadMetricData: true, }; const mockJiraResponse = { @@ -1036,6 +1037,120 @@ describe('saveMetricsSetting reducer', () => { ); }); + describe('should update metrics when reload metric page', () => { + it.each([{ isProjectCreated: false }, { isProjectCreated: true }])( + 'should update classification correctly when reload metrics page', + (mockData) => { + const savedMetricsSetting = saveMetricsSettingReducer( + { + ...initState, + firstTimeRoadMetricData: false, + targetFields: [ + { key: 'issuetype', name: 'Issue Type', flag: false }, + { key: 'parent', name: 'Parent', flag: true }, + ], + }, + updateMetricsState({ + ...mockJiraResponse, + ...mockData, + targetFields: [ + { + key: 'parent', + name: 'Parent', + flag: false, + }, + { + key: 'customfield_10061', + name: 'Story testing', + flag: false, + }, + ], + }), + ); + expect(savedMetricsSetting.targetFields).toEqual([ + { key: 'parent', name: 'Parent', flag: true }, + { key: 'customfield_10061', name: 'Story testing', flag: false }, + ]); + }, + ); + + it.each([{ isProjectCreated: true }, { isProjectCreated: false }])( + 'should update board crews user correctly when reload metrics page', + (mockData) => { + const savedMetricsSetting = saveMetricsSettingReducer( + { + ...initState, + firstTimeRoadMetricData: false, + users: ['User A', 'User B', 'C'], + }, + updateMetricsState({ ...mockJiraResponse, ...mockData }), + ); + expect(savedMetricsSetting.users).toEqual(['User A', 'User B']); + }, + ); + + it.each([CYCLE_TIME_SETTINGS_TYPES.BY_COLUMN, CYCLE_TIME_SETTINGS_TYPES.BY_STATUS])( + 'should update cycle time settings correctly when reload metrics page', + (cycleTimeSettingsType) => { + const savedMetricsSetting = saveMetricsSettingReducer( + { + ...initState, + firstTimeRoadMetricData: false, + cycleTimeSettingsType, + cycleTimeSettings: [ + { + column: 'TODO', + status: 'TODO', + value: 'To do', + }, + { + column: 'Doing', + status: 'DOING', + value: 'In Dev', + }, + { + column: 'Blocked', + status: 'BLOCKED', + value: 'Block', + }, + ], + }, + updateMetricsState({ + ...mockJiraResponse, + jiraColumns: [ + { + key: 'To Do', + value: { + name: 'TODO', + statuses: ['TODO'], + }, + }, + { + key: 'In Progress', + value: { + name: 'Doing', + statuses: ['DOING'], + }, + }, + ], + }), + ); + expect(savedMetricsSetting.cycleTimeSettings).toEqual([ + { + column: 'TODO', + status: 'TODO', + value: 'To do', + }, + { + column: 'Doing', + status: 'DOING', + value: 'In Dev', + }, + ]); + }, + ); + }); + it('should set warningMessage have value when the values in the import file are less than those in the response', () => { const mockUpdateMetricsStateArguments = { ...mockJiraResponse, diff --git a/frontend/__tests__/utils/Util.test.tsx b/frontend/__tests__/utils/Util.test.tsx index 1c90402553..44895773a9 100644 --- a/frontend/__tests__/utils/Util.test.tsx +++ b/frontend/__tests__/utils/Util.test.tsx @@ -1,19 +1,21 @@ import { + convertCycleTimeSettings, exportToJsonFile, filterAndMapCycleTimeSettings, findCaseInsensitiveType, + formatDuplicatedNameWithSuffix, formatMillisecondsToHours, formatMinToHours, + getDisabledOptions, getJiraBoardToken, getRealDoneStatus, - transformToCleanedBuildKiteEmoji, - formatDuplicatedNameWithSuffix, - getDisabledOptions, + getSortedAndDeduplicationBoardingMapping, sortDisabledOptions, + transformToCleanedBuildKiteEmoji, } from '@src/utils/util'; import { CleanedBuildKiteEmoji, OriginBuildKiteEmoji } from '@src/constants/emojis/emoji'; -import { CYCLE_TIME_SETTINGS_TYPES } from '@src/constants/resources'; -import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; +import { CYCLE_TIME_SETTINGS_TYPES, METRICS_CONSTANTS } from '@src/constants/resources'; +import { ICycleTimeSetting, IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { EMPTY_STRING } from '@src/constants/commons'; import { PIPELINE_TOOL_TYPES } from '../fixtures'; @@ -270,3 +272,132 @@ describe('formatDuplicatedNameWithSuffix function', () => { expect(result).toStrictEqual(expectResult); }); }); + +describe('getSortedAndDeduplicationBoardingMapping function', () => { + it('should sorted and deduplication boarding mapping', () => { + const boardingMapping: ICycleTimeSetting[] = [ + METRICS_CONSTANTS.cycleTimeEmptyStr, + METRICS_CONSTANTS.analysisValue, + METRICS_CONSTANTS.testingValue, + METRICS_CONSTANTS.doneValue, + METRICS_CONSTANTS.todoValue, + METRICS_CONSTANTS.cycleTimeEmptyStr, + METRICS_CONSTANTS.blockValue, + METRICS_CONSTANTS.inDevValue, + METRICS_CONSTANTS.reviewValue, + METRICS_CONSTANTS.waitingValue, + METRICS_CONSTANTS.reviewValue, + ].map((value) => ({ + value: value, + status: '', + column: '', + })); + const expectResult = [ + METRICS_CONSTANTS.cycleTimeEmptyStr, + METRICS_CONSTANTS.todoValue, + METRICS_CONSTANTS.analysisValue, + METRICS_CONSTANTS.inDevValue, + METRICS_CONSTANTS.blockValue, + METRICS_CONSTANTS.reviewValue, + METRICS_CONSTANTS.waitingValue, + METRICS_CONSTANTS.testingValue, + METRICS_CONSTANTS.doneValue, + ]; + const result = getSortedAndDeduplicationBoardingMapping(boardingMapping); + expect(result).toStrictEqual(expectResult); + }); +}); + +describe('convertCycleTimeSettings function', () => { + const mockCycleTime = [ + { + column: 'TODO', + status: 'TODO', + value: 'To do', + }, + { + column: 'Doing', + status: 'DOING', + value: 'In Dev', + }, + { + column: 'Blocked', + status: 'BLOCKED', + value: 'Block', + }, + { + column: 'Review', + status: 'REVIEW', + value: 'Review', + }, + { + column: 'READY FOR TESTING', + status: 'WAIT FOR TEST', + value: 'Waiting for testing', + }, + { + column: 'Testing', + status: 'TESTING', + value: 'Testing', + }, + { + column: 'Done', + status: 'DONE', + value: '', + }, + ]; + it('convert cycle time settings correctly by status', () => { + const expectResult = [ + { + TODO: 'To do', + }, + { + DOING: 'In Dev', + }, + { + BLOCKED: 'Block', + }, + { + REVIEW: 'Review', + }, + { + 'WAIT FOR TEST': 'Waiting for testing', + }, + { + TESTING: 'Testing', + }, + { + DONE: '', + }, + ]; + const result = convertCycleTimeSettings(CYCLE_TIME_SETTINGS_TYPES.BY_STATUS, mockCycleTime); + expect(result).toStrictEqual(expectResult); + }); + it('convert cycle time settings correctly by column', () => { + const expectResult = [ + { + TODO: 'To do', + }, + { + Doing: 'In Dev', + }, + { + Blocked: 'Block', + }, + { + Review: 'Review', + }, + { + 'READY FOR TESTING': 'Waiting for testing', + }, + { + Testing: 'Testing', + }, + { + Done: '----', + }, + ]; + const result = convertCycleTimeSettings(CYCLE_TIME_SETTINGS_TYPES.BY_COLUMN, mockCycleTime); + expect(result).toStrictEqual(expectResult); + }); +}); diff --git a/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePicker.tsx b/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePicker.tsx index 58016b8b59..cbae141887 100644 --- a/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePicker.tsx +++ b/frontend/src/containers/ConfigStep/DateRangePicker/DateRangePicker.tsx @@ -13,7 +13,6 @@ import { } from '@src/constants/resources'; import { initDeploymentFrequencySettings, - saveUsers, updateShouldGetBoardConfig, updateShouldGetPipelineConfig, } from '@src/context/Metrics/metricsSlice'; @@ -53,7 +52,6 @@ export const DateRangePicker = ({ startDate, endDate, index }: IRangePickerProps dispatch(updateShouldGetBoardConfig(true)); dispatch(updateShouldGetPipelineConfig(true)); dispatch(initDeploymentFrequencySettings()); - dispatch(saveUsers([])); }; const changeStartDate = (value: Nullable) => { diff --git a/frontend/src/containers/MetricsStep/Classification/index.tsx b/frontend/src/containers/MetricsStep/Classification/index.tsx index b01cc60ba1..8f0483015b 100644 --- a/frontend/src/containers/MetricsStep/Classification/index.tsx +++ b/frontend/src/containers/MetricsStep/Classification/index.tsx @@ -8,7 +8,7 @@ import { useAppDispatch } from '@src/hooks/useAppDispatch'; import { ALL_OPTION_META } from '@src/constants/resources'; import { Z_INDEX } from '@src/constants/commons'; import { useAppSelector } from '@src/hooks'; -import React, { useMemo } from 'react'; +import React from 'react'; export interface classificationProps { title: string; @@ -21,9 +21,7 @@ export const Classification = ({ targetFields, title, label }: classificationPro const targetFieldsWithSuffix = formatDuplicatedNameWithSuffix(targetFields); const classificationWarningMessage = useAppSelector(selectClassificationWarningMessage); const selectedOptions = targetFieldsWithSuffix.filter(({ flag }) => flag); - const isAllSelected = useMemo(() => { - return selectedOptions.length > 0 && selectedOptions.length === targetFieldsWithSuffix.length; - }, [selectedOptions, targetFieldsWithSuffix]); + const isAllSelected = selectedOptions.length > 0 && selectedOptions.length === targetFieldsWithSuffix.length; const handleChange = (_: React.SyntheticEvent, value: ITargetFieldType[]) => { let nextSelectedOptions: ITargetFieldType[]; diff --git a/frontend/src/containers/MetricsStep/Crews/index.tsx b/frontend/src/containers/MetricsStep/Crews/index.tsx index e4fef55433..9b686cebe6 100644 --- a/frontend/src/containers/MetricsStep/Crews/index.tsx +++ b/frontend/src/containers/MetricsStep/Crews/index.tsx @@ -1,10 +1,10 @@ -import { saveUsers, selectMetricsContent, savePipelineCrews } from '@src/context/Metrics/metricsSlice'; +import { savePipelineCrews, saveUsers, selectMetricsContent } from '@src/context/Metrics/metricsSlice'; import { AssigneeFilter } from '@src/containers/MetricsStep/Crews/AssigneeFilter'; import { MetricsSettingTitle } from '@src/components/Common/MetricsSettingTitle'; import MultiAutoComplete from '@src/components/Common/MultiAutoComplete'; import { WarningMessage } from '@src/containers/MetricsStep/Crews/style'; -import React, { useEffect, useState, useMemo } from 'react'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; +import React, { useEffect, useState } from 'react'; import { FormHelperText } from '@mui/material'; import { useAppSelector } from '@src/hooks'; @@ -18,20 +18,15 @@ interface crewsProps { export const Crews = ({ options, title, label, type = 'board' }: crewsProps) => { const isBoardCrews = type === 'board'; const dispatch = useAppDispatch(); - - const [isEmptyCrewData, setIsEmptyCrewData] = useState(false); const { users, pipelineCrews } = useAppSelector(selectMetricsContent); - const [selectedCrews, setSelectedCrews] = useState([]); + const [selectedCrews, setSelectedCrews] = useState(isBoardCrews ? users : pipelineCrews); const isAllSelected = options.length > 0 && selectedCrews.length === options.length; + const isEmptyCrewData = selectedCrews.length === 0; - useMemo(() => { + useEffect(() => { setSelectedCrews(isBoardCrews ? users : pipelineCrews); }, [users, isBoardCrews, pipelineCrews]); - useEffect(() => { - setIsEmptyCrewData(selectedCrews.length === 0); - }, [selectedCrews]); - const handleCrewChange = (_: React.SyntheticEvent, value: string[]) => { if (value[value.length - 1] === 'All') { setSelectedCrews(selectedCrews.length === options.length ? [] : options); diff --git a/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx b/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx index 751fdedef1..23e43c484d 100644 --- a/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx +++ b/frontend/src/containers/MetricsStep/ReworkSettings/index.tsx @@ -3,35 +3,31 @@ import { updateReworkTimesSettings, selectCycleTimeSettings, } from '@src/context/Metrics/metricsSlice'; +import { getSortedAndDeduplicationBoardingMapping, onlyEmptyAndDoneState } from '@src/utils/util'; 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'; import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; import MultiAutoComplete from '@src/components/Common/MultiAutoComplete'; import { ReworkHeaderWrapper, ReworkSettingsWrapper } from './style'; import { StyledLink } from '@src/containers/MetricsStep/style'; +import { METRICS_CONSTANTS } from '@src/constants/resources'; import { useAppDispatch, useAppSelector } from '@src/hooks'; -import { onlyEmptyAndDoneState } from '@src/utils/util'; import { SingleSelection } from './SingleSelection'; -import React, { useState, useMemo } from 'react'; +import React, { useState } 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 boardingMappingStatus = getSortedAndDeduplicationBoardingMapping(cycleTimeSettings); const boardingMappingHasDoneStatus = boardingMappingStatus.includes(METRICS_CONSTANTS.doneValue); const allStateIsEmpty = boardingMappingStatus.every((value) => value === METRICS_CONSTANTS.cycleTimeEmptyStr); const isOnlyEmptyAndDoneState = onlyEmptyAndDoneState(boardingMappingStatus); - 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 singleOptions = boardingMappingStatus.filter( + (item) => item !== METRICS_CONSTANTS.doneValue && item !== METRICS_CONSTANTS.cycleTimeEmptyStr, + ); const multiOptions = reworkTimesSettings.reworkState ? [ diff --git a/frontend/src/containers/MetricsStep/index.tsx b/frontend/src/containers/MetricsStep/index.tsx index 8a5b151f76..77be71a49e 100644 --- a/frontend/src/containers/MetricsStep/index.tsx +++ b/frontend/src/containers/MetricsStep/index.tsx @@ -13,6 +13,7 @@ import { updateMetricsState, selectShouldGetBoardConfig, updateShouldGetBoardConfig, + updateFirstTimeRoadMetricsBoardData, } from '@src/context/Metrics/metricsSlice'; import { MetricSelectionHeader, @@ -75,6 +76,7 @@ const MetricsStep = () => { dispatch(updateJiraVerifyResponse(res.data)); dispatch(updateMetricsState(merge(res.data, { isProjectCreated: isProjectCreated }))); dispatch(updateShouldGetBoardConfig(false)); + dispatch(updateFirstTimeRoadMetricsBoardData(false)); } }), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/containers/MetricsStepper/index.tsx b/frontend/src/containers/MetricsStepper/index.tsx index 82697ff473..5dae310010 100644 --- a/frontend/src/containers/MetricsStepper/index.tsx +++ b/frontend/src/containers/MetricsStepper/index.tsx @@ -30,17 +30,16 @@ import { StyledStepper, } from './style'; import { - ICycleTimeSetting, - savedMetricsSettingState, + ISavedMetricsSettingState, selectCycleTimeSettings, selectMetricsContent, } from '@src/context/Metrics/metricsSlice'; import { backStep, nextStep, selectStepNumber, updateTimeStamp } from '@src/context/stepper/StepperSlice'; import { useMetricsStepValidationCheckContext } from '@src/hooks/useMetricsStepValidationCheckContext'; +import { convertCycleTimeSettings, exportToJsonFile, onlyEmptyAndDoneState } from '@src/utils/util'; 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 { exportToJsonFile, onlyEmptyAndDoneState } from '@src/utils/util'; import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { getFormMeta } from '@src/context/meta/metaSlice'; import SaveAltIcon from '@mui/icons-material/SaveAlt'; @@ -176,7 +175,7 @@ const MetricsStepper = () => { onlyIncludeReworkMetrics, ]); - const filterMetricsConfig = (metricsConfig: savedMetricsSettingState) => { + const filterMetricsConfig = (metricsConfig: ISavedMetricsSettingState) => { return Object.fromEntries( Object.entries(metricsConfig).filter(([, value]) => { /* istanbul ignore next */ @@ -230,16 +229,7 @@ const MetricsStepper = () => { cycleTime: cycleTimeSettings ? { type: cycleTimeSettingsType, - jiraColumns: - cycleTimeSettingsType === CYCLE_TIME_SETTINGS_TYPES.BY_COLUMN - ? ([...new Set(cycleTimeSettings.map(({ column }: ICycleTimeSetting) => column))] as string[]).map( - (uniqueColumn) => ({ - [uniqueColumn]: - cycleTimeSettings.find(({ column }: ICycleTimeSetting) => column === uniqueColumn)?.value || - METRICS_CONSTANTS.cycleTimeEmptyStr, - }), - ) - : cycleTimeSettings?.map(({ status, value }: ICycleTimeSetting) => ({ [status]: value })), + jiraColumns: convertCycleTimeSettings(cycleTimeSettingsType, cycleTimeSettings), treatFlagCardAsBlock, } : undefined, diff --git a/frontend/src/context/Metrics/metricsSlice.ts b/frontend/src/context/Metrics/metricsSlice.ts index dcb278db0b..2b6316b564 100644 --- a/frontend/src/context/Metrics/metricsSlice.ts +++ b/frontend/src/context/Metrics/metricsSlice.ts @@ -5,6 +5,7 @@ import { MESSAGE, METRICS_CONSTANTS, } from '@src/constants/resources'; +import { convertCycleTimeSettings, getSortedAndDeduplicationBoardingMapping } from '@src/utils/util'; import { pipeline } from '@src/context/config/pipelineTool/verifyResponseSlice'; import { createSlice } from '@reduxjs/toolkit'; import camelCase from 'lodash.camelcase'; @@ -37,7 +38,7 @@ export interface ICycleTimeSetting { value: string; } -export interface savedMetricsSettingState { +export interface ISavedMetricsSettingState { shouldGetBoardConfig: boolean; shouldGetPipeLineConfig: boolean; jiraColumns: { key: string; value: { name: string; statuses: string[] } }[]; @@ -51,6 +52,7 @@ export interface savedMetricsSettingState { leadTimeForChanges: IPipelineConfig[]; treatFlagCardAsBlock: boolean; assigneeFilter: string; + firstTimeRoadMetricData: boolean; importedData: { importedCrews: string[]; importedAssigneeFilter: string; @@ -71,7 +73,7 @@ export interface savedMetricsSettingState { deploymentWarningMessage: IPipelineWarningMessageConfig[]; } -const initialState: savedMetricsSettingState = { +const initialState: ISavedMetricsSettingState = { shouldGetBoardConfig: false, shouldGetPipeLineConfig: false, jiraColumns: [], @@ -85,6 +87,7 @@ const initialState: savedMetricsSettingState = { leadTimeForChanges: [{ id: 0, organization: '', pipelineName: '', step: '', branches: [] }], treatFlagCardAsBlock: true, assigneeFilter: ASSIGNEE_FILTER_TYPES.LAST_ASSIGNEE, + firstTimeRoadMetricData: true, importedData: { importedCrews: [], importedAssigneeFilter: ASSIGNEE_FILTER_TYPES.LAST_ASSIGNEE, @@ -140,8 +143,21 @@ const findKeyByValues = (arrayA: { [key: string]: string }[], arrayB: string[]): return `The value of ${matchingKeys} in imported json is not in dropdown list now. Please select a value for it!`; }; -const setSelectUsers = (users: string[], importedCrews: string[]) => - users.filter((item: string) => importedCrews?.includes(item)); +const setImportSelectUsers = (metricsState: ISavedMetricsSettingState, users: string[], importedCrews: string[]) => { + if (metricsState.firstTimeRoadMetricData) { + return users.filter((item: string) => importedCrews?.includes(item)); + } else { + return users.filter((item: string) => metricsState.users?.includes(item)); + } +}; + +const setCreateSelectUsers = (metricsState: ISavedMetricsSettingState, users: string[]) => { + if (metricsState.firstTimeRoadMetricData) { + return users; + } else { + return users.filter((item: string) => metricsState.users?.includes(item)); + } +}; const setPipelineCrews = (isProjectCreated: boolean, pipelineCrews: string[], importedPipelineCrews: string[]) => { if (_.isEmpty(pipelineCrews)) { @@ -154,20 +170,70 @@ const setPipelineCrews = (isProjectCreated: boolean, pipelineCrews: string[], im }; const setSelectTargetFields = ( + state: ISavedMetricsSettingState, + targetFields: { name: string; key: string; flag: boolean }[], + isProjectCreated: boolean, +) => { + if (isProjectCreated) { + return setCreateSelectTargetFields(state, targetFields); + } else { + return setImportSelectTargetFields(state, targetFields); + } +}; + +const setImportSelectTargetFields = ( + state: ISavedMetricsSettingState, targetFields: { name: string; key: string; flag: boolean }[], - importedClassification: string[], -) => - targetFields.map((item: { name: string; key: string; flag: boolean }) => ({ +) => { + if (state.firstTimeRoadMetricData) { + return targetFields.map((item: { name: string; key: string; flag: boolean }) => ({ + ...item, + flag: state.importedData.importedClassification.includes(item.key), + })); + } else { + return getTargetFieldsIntersection(state, targetFields); + } +}; + +const setCreateSelectTargetFields = ( + state: ISavedMetricsSettingState, + targetFields: { + name: string; + key: string; + flag: boolean; + }[], +) => { + if (state.firstTimeRoadMetricData) { + return targetFields; + } else { + return getTargetFieldsIntersection(state, targetFields); + } +}; + +const getTargetFieldsIntersection = ( + state: ISavedMetricsSettingState, + targetFields: { + name: string; + key: string; + flag: boolean; + }[], +) => { + const selectedFields = state.targetFields.filter((value) => value.flag).map((value) => value.key); + return targetFields.map((item: { name: string; key: string; flag: boolean }) => ({ ...item, - flag: importedClassification?.includes(item.key), + flag: selectedFields.includes(item.key), })); +}; const getCycleTimeSettingsByColumn = ( + state: ISavedMetricsSettingState, jiraColumns: { key: string; value: { name: string; statuses: string[] } }[], - importedCycleTimeSettings: { [key: string]: string }[], -) => - jiraColumns.flatMap(({ value: { name, statuses } }) => { - const importItem = importedCycleTimeSettings.find((i) => Object.keys(i).includes(name)); +) => { + const preCycleTimeSettings = state.firstTimeRoadMetricData + ? state.importedData.importedCycleTime.importedCycleTimeSettings + : convertCycleTimeSettings(state.cycleTimeSettingsType, state.cycleTimeSettings); + return jiraColumns.flatMap(({ value: { name, statuses } }) => { + const importItem = preCycleTimeSettings.find((i) => Object.keys(i).includes(name)); const isValidValue = importItem && CYCLE_TIME_LIST.includes(Object.values(importItem)[0]); return statuses.map((status) => ({ column: name, @@ -175,14 +241,18 @@ const getCycleTimeSettingsByColumn = ( value: isValidValue ? (Object.values(importItem)[0] as string) : METRICS_CONSTANTS.cycleTimeEmptyStr, })); }); +}; const getCycleTimeSettingsByStatus = ( + state: ISavedMetricsSettingState, jiraColumns: { key: string; value: { name: string; statuses: string[] } }[], - importedCycleTimeSettings: { [key: string]: string }[], -) => - jiraColumns.flatMap(({ value: { name, statuses } }) => +) => { + const preCycleTimeSettings = state.firstTimeRoadMetricData + ? state.importedData.importedCycleTime.importedCycleTimeSettings + : convertCycleTimeSettings(state.cycleTimeSettingsType, state.cycleTimeSettings); + return jiraColumns.flatMap(({ value: { name, statuses } }) => statuses.map((status) => { - const importItem = importedCycleTimeSettings.find((i) => Object.keys(i).includes(status)); + const importItem = preCycleTimeSettings.find((i) => Object.keys(i).includes(status)); const isValidValue = importItem && CYCLE_TIME_LIST.includes(Object.values(importItem)[0]); return { column: name, @@ -191,6 +261,7 @@ const getCycleTimeSettingsByStatus = ( }; }), ); +}; const getSelectedDoneStatus = ( jiraColumns: { key: string; value: { name: string; statuses: string[] } }[], @@ -206,6 +277,19 @@ const getSelectedDoneStatus = ( return status.filter((item: string) => importedDoneStatus.includes(item)); }; +function resetReworkTimeSettingWhenMappingModified(preJiraColumnsValue: string[], state: ISavedMetricsSettingState) { + const boardingMapping = getSortedAndDeduplicationBoardingMapping(state.cycleTimeSettings).filter( + (item) => item !== METRICS_CONSTANTS.cycleTimeEmptyStr, + ); + if (state.firstTimeRoadMetricData || _.isEqual(preJiraColumnsValue, boardingMapping)) { + return; + } + state.importedData.reworkTimesSettings = { + reworkState: null, + excludeStates: [], + }; +} + export const metricsSlice = createSlice({ name: 'metrics', initialState, @@ -293,10 +377,13 @@ export const metricsSlice = createSlice({ const { targetFields, users, jiraColumns, isProjectCreated, ignoredTargetFields } = action.payload; const { importedCrews, importedClassification, importedCycleTime, importedDoneStatus, importedAssigneeFilter } = state.importedData; - state.users = isProjectCreated ? users : setSelectUsers(users, importedCrews); - state.targetFields = isProjectCreated - ? targetFields - : setSelectTargetFields(targetFields, importedClassification); + const preJiraColumnsValue = getSortedAndDeduplicationBoardingMapping(state.cycleTimeSettings).filter( + (item) => item !== METRICS_CONSTANTS.cycleTimeEmptyStr, + ); + state.users = isProjectCreated + ? setCreateSelectUsers(state, users) + : setImportSelectUsers(state, users, importedCrews); + state.targetFields = setSelectTargetFields(state, targetFields, isProjectCreated); if (!isProjectCreated && importedCycleTime?.importedCycleTimeSettings?.length > 0) { const importedCycleTimeSettingsKeys = importedCycleTime.importedCycleTimeSettings.flatMap((obj) => @@ -349,13 +436,13 @@ export const metricsSlice = createSlice({ } else { state.classificationWarningMessage = null; } - if (jiraColumns) { state.cycleTimeSettings = state.cycleTimeSettingsType === CYCLE_TIME_SETTINGS_TYPES.BY_COLUMN - ? getCycleTimeSettingsByColumn(jiraColumns, importedCycleTime.importedCycleTimeSettings) - : getCycleTimeSettingsByStatus(jiraColumns, importedCycleTime.importedCycleTimeSettings); + ? getCycleTimeSettingsByColumn(state, jiraColumns) + : getCycleTimeSettingsByStatus(state, jiraColumns); } + resetReworkTimeSettingWhenMappingModified(preJiraColumnsValue, state); if (!isProjectCreated && importedDoneStatus.length > 0) { const selectedDoneStatus = getSelectedDoneStatus(jiraColumns, state.cycleTimeSettings, importedDoneStatus); @@ -501,6 +588,10 @@ export const metricsSlice = createSlice({ updateReworkTimesSettings: (state, action) => { state.importedData.reworkTimesSettings = action.payload; }, + + updateFirstTimeRoadMetricsBoardData: (state, action) => { + state.firstTimeRoadMetricData = action.payload; + }, }, }); @@ -526,6 +617,7 @@ export const { updateShouldGetBoardConfig, updateShouldGetPipelineConfig, updateReworkTimesSettings, + updateFirstTimeRoadMetricsBoardData, } = metricsSlice.actions; export const selectShouldGetBoardConfig = (state: RootState) => state.metrics.shouldGetBoardConfig; diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index b3559a4418..f47063b302 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -1,5 +1,5 @@ +import { CYCLE_TIME_LIST, CYCLE_TIME_SETTINGS_TYPES, METRICS_CONSTANTS } from '@src/constants/resources'; import { CleanedBuildKiteEmoji, OriginBuildKiteEmoji } from '@src/constants/emojis/emoji'; -import { CYCLE_TIME_SETTINGS_TYPES, METRICS_CONSTANTS } from '@src/constants/resources'; import { ICycleTimeSetting, IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { ITargetFieldType } from '@src/components/Common/MultiAutoComplete/styles'; import { DATE_FORMAT_TEMPLATE } from '@src/constants/template'; @@ -122,7 +122,29 @@ export const formatDuplicatedNameWithSuffix = (data: ITargetFieldType[]) => { }); }; +export const getSortedAndDeduplicationBoardingMapping = (boardMapping: ICycleTimeSetting[]) => { + return [...new Set(boardMapping.map((item) => item.value))].sort((a, b) => { + return CYCLE_TIME_LIST.indexOf(a) - CYCLE_TIME_LIST.indexOf(b); + }); +}; + export const onlyEmptyAndDoneState = (boardingMappingStates: string[]) => isEqual(boardingMappingStates, [METRICS_CONSTANTS.doneValue]) || isEqual(boardingMappingStates, [METRICS_CONSTANTS.cycleTimeEmptyStr, METRICS_CONSTANTS.doneValue]) || isEqual(boardingMappingStates, [METRICS_CONSTANTS.doneValue, METRICS_CONSTANTS.cycleTimeEmptyStr]); + +export function convertCycleTimeSettings( + cycleTimeSettingsType: CYCLE_TIME_SETTINGS_TYPES, + cycleTimeSettings: ICycleTimeSetting[], +) { + if (cycleTimeSettingsType === CYCLE_TIME_SETTINGS_TYPES.BY_COLUMN) { + return ([...new Set(cycleTimeSettings.map(({ column }: ICycleTimeSetting) => column))] as string[]).map( + (uniqueColumn) => ({ + [uniqueColumn]: + cycleTimeSettings.find(({ column }: ICycleTimeSetting) => column === uniqueColumn)?.value || + METRICS_CONSTANTS.cycleTimeEmptyStr, + }), + ); + } + return cycleTimeSettings?.map(({ status, value }: ICycleTimeSetting) => ({ [status]: value })); +}