@@ -30,6 +30,7 @@ import { | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures'; | |||
import { getOperatorLabel } from '../../../helpers/qualityGates'; | |||
import { | |||
getComponentDrilldownUrl, | |||
getComponentIssuesUrl, | |||
@@ -155,11 +156,7 @@ export default class QualityGateCondition extends React.PureComponent<Props> { | |||
const threshold = (condition.level === 'ERROR' ? condition.error : condition.warning) as string; | |||
const actual = (condition.period ? measure.period?.value : measure.value) as string; | |||
let operator = translate('quality_gates.operator', condition.op); | |||
if (metric.type === MetricType.Rating) { | |||
operator = translate('quality_gates.operator', condition.op, 'rating'); | |||
} | |||
const operator = getOperatorLabel(condition.op, metric); | |||
return this.wrapWithLink( | |||
<div className="sw-flex sw-items-center sw-p-2"> |
@@ -21,10 +21,7 @@ import { ButtonPrimary, FormField, Modal, RadioButton } from 'design-system'; | |||
import * as React from 'react'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
import { | |||
useCreateConditionMutation, | |||
useUpdateConditionMutation, | |||
} from '../../../queries/quality-gates'; | |||
import { useCreateConditionMutation } from '../../../queries/quality-gates'; | |||
import { Condition, Metric, QualityGate } from '../../../types/types'; | |||
import { getPossibleOperators } from '../utils'; | |||
import ConditionOperator from './ConditionOperator'; | |||
@@ -32,32 +29,19 @@ import MetricSelect from './MetricSelect'; | |||
import ThresholdInput from './ThresholdInput'; | |||
interface Props { | |||
condition?: Condition; | |||
metric?: Metric; | |||
metrics?: Metric[]; | |||
header: string; | |||
metrics: Metric[]; | |||
onClose: () => void; | |||
qualityGate: QualityGate; | |||
} | |||
const ADD_CONDITION_MODAL_ID = 'add-condition-modal'; | |||
export default function ConditionModal({ | |||
condition, | |||
metric, | |||
metrics, | |||
header, | |||
onClose, | |||
qualityGate, | |||
}: Readonly<Props>) { | |||
const [errorThreshold, setErrorThreshold] = React.useState(condition ? condition.error : ''); | |||
export default function AddConditionModal({ metrics, onClose, qualityGate }: Readonly<Props>) { | |||
const [errorThreshold, setErrorThreshold] = React.useState(''); | |||
const [scope, setScope] = React.useState<'new' | 'overall'>('new'); | |||
const [selectedMetric, setSelectedMetric] = React.useState<Metric | undefined>(metric); | |||
const [selectedOperator, setSelectedOperator] = React.useState<string | undefined>( | |||
condition ? condition.op : undefined, | |||
); | |||
const [selectedMetric, setSelectedMetric] = React.useState<Metric | undefined>(); | |||
const [selectedOperator, setSelectedOperator] = React.useState<string | undefined>(); | |||
const { mutateAsync: createCondition } = useCreateConditionMutation(qualityGate.name); | |||
const { mutateAsync: updateCondition } = useUpdateConditionMutation(qualityGate.name); | |||
const getSinglePossibleOperator = (metric: Metric) => { | |||
const operators = getPossibleOperators(metric); | |||
@@ -73,10 +57,7 @@ export default function ConditionModal({ | |||
op: getSinglePossibleOperator(selectedMetric) ?? selectedOperator, | |||
error: errorThreshold, | |||
}; | |||
const submitPromise = condition | |||
? updateCondition({ id: condition.id, ...newCondition }) | |||
: createCondition(newCondition); | |||
await submitPromise; | |||
await createCondition(newCondition); | |||
onClose(); | |||
} | |||
}; | |||
@@ -84,7 +65,7 @@ export default function ConditionModal({ | |||
const handleScopeChange = (scope: 'new' | 'overall') => { | |||
let correspondingMetric; | |||
if (selectedMetric && metrics) { | |||
if (selectedMetric) { | |||
const correspondingMetricKey = | |||
scope === 'new' ? `new_${selectedMetric.key}` : selectedMetric.key.replace(/^new_/, ''); | |||
correspondingMetric = metrics.find((m) => m.key === correspondingMetricKey); | |||
@@ -110,42 +91,34 @@ export default function ConditionModal({ | |||
const renderBody = () => { | |||
return ( | |||
<form id={ADD_CONDITION_MODAL_ID} onSubmit={handleFormSubmit}> | |||
{metric === undefined && ( | |||
<FormField label={translate('quality_gates.conditions.where')}> | |||
<div className="sw-flex sw-gap-4"> | |||
<RadioButton checked={scope === 'new'} onCheck={handleScopeChange} value="new"> | |||
<span data-test="quality-gates__condition-scope-new"> | |||
{translate('quality_gates.conditions.new_code')} | |||
</span> | |||
</RadioButton> | |||
<RadioButton | |||
checked={scope === 'overall'} | |||
onCheck={handleScopeChange} | |||
value="overall" | |||
> | |||
<span data-test="quality-gates__condition-scope-overall"> | |||
{translate('quality_gates.conditions.overall_code')} | |||
</span> | |||
</RadioButton> | |||
</div> | |||
</FormField> | |||
)} | |||
<form onSubmit={handleFormSubmit} id={ADD_CONDITION_MODAL_ID}> | |||
<FormField label={translate('quality_gates.conditions.where')}> | |||
<div className="sw-flex sw-gap-4"> | |||
<RadioButton checked={scope === 'new'} onCheck={handleScopeChange} value="new"> | |||
<span data-test="quality-gates__condition-scope-new"> | |||
{translate('quality_gates.conditions.new_code')} | |||
</span> | |||
</RadioButton> | |||
<RadioButton checked={scope === 'overall'} onCheck={handleScopeChange} value="overall"> | |||
<span data-test="quality-gates__condition-scope-overall"> | |||
{translate('quality_gates.conditions.overall_code')} | |||
</span> | |||
</RadioButton> | |||
</div> | |||
</FormField> | |||
<FormField | |||
description={metric && getLocalizedMetricName(metric)} | |||
description={selectedMetric && getLocalizedMetricName(selectedMetric)} | |||
htmlFor="condition-metric" | |||
label={translate('quality_gates.conditions.fails_when')} | |||
> | |||
{metrics && ( | |||
<MetricSelect | |||
metric={selectedMetric} | |||
metricsArray={metrics.filter((m) => | |||
scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key), | |||
)} | |||
onMetricChange={handleMetricChange} | |||
/> | |||
)} | |||
<MetricSelect | |||
metric={selectedMetric} | |||
metricsArray={metrics.filter((m) => | |||
scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key), | |||
)} | |||
onMetricChange={handleMetricChange} | |||
/> | |||
</FormField> | |||
{selectedMetric && ( | |||
@@ -182,7 +155,7 @@ export default function ConditionModal({ | |||
<Modal | |||
isScrollable={false} | |||
isOverflowVisible | |||
headerTitle={header} | |||
headerTitle={translate('quality_gates.add_condition')} | |||
onClose={onClose} | |||
body={renderBody()} | |||
primaryButton={ | |||
@@ -193,7 +166,7 @@ export default function ConditionModal({ | |||
form={ADD_CONDITION_MODAL_ID} | |||
type="submit" | |||
> | |||
{header} | |||
{translate('quality_gates.add_condition')} | |||
</ButtonPrimary> | |||
} | |||
secondaryButtonLabel={translate('close')} |
@@ -33,12 +33,12 @@ import { | |||
import * as React from 'react'; | |||
import { useMetrics } from '../../../app/components/metrics/withMetricsContext'; | |||
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getOperatorLabel } from '../../../helpers/qualityGates'; | |||
import { useDeleteConditionMutation } from '../../../queries/quality-gates'; | |||
import { MetricType } from '../../../types/metrics'; | |||
import { CaycStatus, Condition as ConditionType, Metric, QualityGate } from '../../../types/types'; | |||
import { getLocalizedMetricNameNoDiffMetric, isConditionWithFixedValue } from '../utils'; | |||
import ConditionModal from './ConditionModal'; | |||
import ConditionValue from './ConditionValue'; | |||
import EditConditionModal from './EditConditionModal'; | |||
export enum ConditionChange { | |||
Added = 'added', | |||
@@ -67,6 +67,7 @@ export default function ConditionComponent({ | |||
const [modal, setModal] = React.useState(false); | |||
const { mutateAsync: deleteCondition } = useDeleteConditionMutation(qualityGate.name); | |||
const metrics = useMetrics(); | |||
const { op = 'GT' } = condition; | |||
const handleOpenUpdate = () => { | |||
setModal(true); | |||
@@ -84,13 +85,6 @@ export default function ConditionComponent({ | |||
setDeleteFormOpen(false); | |||
}; | |||
const renderOperator = () => { | |||
const { op = 'GT' } = condition; | |||
return metric.type === MetricType.Rating | |||
? translate('quality_gates.operator', op, 'rating') | |||
: translate('quality_gates.operator', op); | |||
}; | |||
const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant; | |||
return ( | |||
@@ -100,7 +94,7 @@ export default function ConditionComponent({ | |||
{metric.hidden && <TextError className="sw-ml-1" text={translate('deprecated')} />} | |||
</ContentCell> | |||
<ContentCell className="sw-whitespace-nowrap">{renderOperator()}</ContentCell> | |||
<ContentCell className="sw-whitespace-nowrap">{getOperatorLabel(op, metric)}</ContentCell> | |||
<NumericalCell className="sw-whitespace-nowrap"> | |||
<ConditionValue | |||
@@ -126,7 +120,7 @@ export default function ConditionComponent({ | |||
size="small" | |||
/> | |||
{modal && ( | |||
<ConditionModal | |||
<EditConditionModal | |||
condition={condition} | |||
header={translate('quality_gates.update_condition')} | |||
metric={metric} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import { InputSelect, Note } from 'design-system'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getOperatorLabel } from '../../../helpers/qualityGates'; | |||
import { Metric } from '../../../types/types'; | |||
import { getPossibleOperators } from '../utils'; | |||
@@ -34,18 +34,12 @@ export default class ConditionOperator extends React.PureComponent<Props> { | |||
this.props.onOperatorChange(value); | |||
}; | |||
getLabel(op: string, metric: Metric) { | |||
return metric.type === 'RATING' | |||
? translate('quality_gates.operator', op, 'rating') | |||
: translate('quality_gates.operator', op); | |||
} | |||
render() { | |||
const operators = getPossibleOperators(this.props.metric); | |||
if (Array.isArray(operators)) { | |||
const operatorOptions = operators.map((op) => { | |||
const label = this.getLabel(op, this.props.metric); | |||
const label = getOperatorLabel(op, this.props.metric); | |||
return { label, value: op }; | |||
}); | |||
@@ -64,6 +58,6 @@ export default class ConditionOperator extends React.PureComponent<Props> { | |||
); | |||
} | |||
return <Note className="sw-w-abs-150">{this.getLabel(operators, this.props.metric)}</Note>; | |||
return <Note className="sw-w-abs-150">{getOperatorLabel(operators, this.props.metric)}</Note>; | |||
} | |||
} |
@@ -42,11 +42,11 @@ import { Feature } from '../../../types/features'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { CaycStatus, Condition as ConditionType, QualityGate } from '../../../types/types'; | |||
import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils'; | |||
import AddConditionModal from './AddConditionModal'; | |||
import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide'; | |||
import CaycCompliantBanner from './CaycCompliantBanner'; | |||
import CaycCondition from './CaycCondition'; | |||
import CaycFixOptimizeBanner from './CaycFixOptimizeBanner'; | |||
import ConditionModal from './ConditionModal'; | |||
import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; | |||
import ConditionsTable from './ConditionsTable'; | |||
@@ -110,12 +110,7 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>) | |||
(metric, condition) => metric.key === condition.metric, | |||
); | |||
return ( | |||
<ConditionModal | |||
header={translate('quality_gates.add_condition')} | |||
metrics={availableMetrics} | |||
onClose={onClose} | |||
qualityGate={qualityGate} | |||
/> | |||
<AddConditionModal metrics={availableMetrics} onClose={onClose} qualityGate={qualityGate} /> | |||
); | |||
}, | |||
[metrics, qualityGate], |
@@ -0,0 +1,131 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { ButtonPrimary, FormField, Highlight, Modal, Note } from 'design-system'; | |||
import { isArray } from 'lodash'; | |||
import * as React from 'react'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import { useUpdateConditionMutation } from '../../../queries/quality-gates'; | |||
import { Condition, Metric, QualityGate } from '../../../types/types'; | |||
import { getPossibleOperators } from '../utils'; | |||
import ConditionOperator from './ConditionOperator'; | |||
import ThresholdInput from './ThresholdInput'; | |||
interface Props { | |||
condition: Condition; | |||
metric: Metric; | |||
header: string; | |||
onClose: () => void; | |||
qualityGate: QualityGate; | |||
} | |||
const EDIT_CONDITION_MODAL_ID = 'edit-condition-modal'; | |||
export default function EditConditionModal({ | |||
condition, | |||
metric, | |||
onClose, | |||
qualityGate, | |||
}: Readonly<Props>) { | |||
const [errorThreshold, setErrorThreshold] = React.useState(condition ? condition.error : ''); | |||
const [selectedOperator, setSelectedOperator] = React.useState<string | undefined>( | |||
condition ? condition.op : undefined, | |||
); | |||
const { mutateAsync: updateCondition } = useUpdateConditionMutation(qualityGate.name); | |||
const getSinglePossibleOperator = (metric: Metric) => { | |||
const operators = getPossibleOperators(metric); | |||
return isArray(operators) ? selectedOperator : operators; | |||
}; | |||
const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const newCondition: Omit<Condition, 'id'> = { | |||
metric: metric.key, | |||
op: getSinglePossibleOperator(metric), | |||
error: errorThreshold, | |||
}; | |||
await updateCondition({ id: condition.id, ...newCondition }); | |||
onClose(); | |||
}; | |||
const handleErrorChange = (error: string) => { | |||
setErrorThreshold(error); | |||
}; | |||
const handleOperatorChange = (op: string) => { | |||
setSelectedOperator(op); | |||
}; | |||
const renderBody = () => { | |||
return ( | |||
<form onSubmit={handleFormSubmit} id={EDIT_CONDITION_MODAL_ID}> | |||
<span className="sw-flex sw-flex-col sw-w-full sw-mb-6" aria-hidden="true"> | |||
<Highlight className="sw-mb-2 sw-flex sw-items-center sw-gap-2"> | |||
<span>{translate('quality_gates.conditions.fails_when')}</span> | |||
</Highlight> | |||
<Note className="sw-mt-2">{getLocalizedMetricName(metric)}</Note> | |||
</span> | |||
<div className="sw-flex sw-gap-2"> | |||
<FormField | |||
className="sw-mb-0" | |||
htmlFor="condition-operator" | |||
label={translate('quality_gates.conditions.operator')} | |||
> | |||
<ConditionOperator | |||
metric={metric} | |||
onOperatorChange={handleOperatorChange} | |||
op={selectedOperator} | |||
/> | |||
</FormField> | |||
<FormField | |||
htmlFor="condition-threshold" | |||
label={translate('quality_gates.conditions.value')} | |||
> | |||
<ThresholdInput | |||
metric={metric} | |||
name="error" | |||
onChange={handleErrorChange} | |||
value={errorThreshold} | |||
/> | |||
</FormField> | |||
</div> | |||
</form> | |||
); | |||
}; | |||
return ( | |||
<Modal | |||
isScrollable={false} | |||
isOverflowVisible | |||
headerTitle={translate('quality_gates.update_condition')} | |||
onClose={onClose} | |||
body={renderBody()} | |||
primaryButton={ | |||
<ButtonPrimary form={EDIT_CONDITION_MODAL_ID} type="submit"> | |||
{translate('quality_gates.update_condition')} | |||
</ButtonPrimary> | |||
} | |||
secondaryButtonLabel={translate('close')} | |||
/> | |||
); | |||
} |
@@ -26,7 +26,7 @@ import { searchProjects, searchUsers } from '../../../../api/quality-gates'; | |||
import { dismissNotice } from '../../../../api/users'; | |||
import { mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { RenderContext, renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; | |||
import { byRole } from '../../../../helpers/testSelector'; | |||
import { byRole, byTestId } from '../../../../helpers/testSelector'; | |||
import { Feature } from '../../../../types/features'; | |||
import { CaycStatus } from '../../../../types/types'; | |||
import { NoticeType } from '../../../../types/users'; | |||
@@ -218,49 +218,52 @@ it('should be able to add a condition', async () => { | |||
// On new code | |||
await user.click(await screen.findByText('quality_gates.add_condition')); | |||
let dialog = within(screen.getByRole('dialog')); | |||
const dialog = byRole('dialog'); | |||
await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.new_code' })); | |||
await selectEvent.select(dialog.getByRole('combobox'), ['Issues']); | |||
await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' })); | |||
await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.new_code' }).get()); | |||
await selectEvent.select(dialog.byRole('combobox').get(), 'Issues'); | |||
await user.click( | |||
await dialog.byRole('textbox', { name: 'quality_gates.conditions.value' }).find(), | |||
); | |||
await user.keyboard('12'); | |||
await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' })); | |||
const newConditions = within(await screen.findByTestId('quality-gates__conditions-new')); | |||
expect(await newConditions.findByRole('cell', { name: 'Issues' })).toBeInTheDocument(); | |||
expect(await newConditions.findByRole('cell', { name: '12' })).toBeInTheDocument(); | |||
await user.click(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get()); | |||
const newConditions = byTestId('quality-gates__conditions-new'); | |||
expect(await newConditions.byRole('cell', { name: 'Issues' }).find()).toBeInTheDocument(); | |||
expect(await newConditions.byRole('cell', { name: '12' }).find()).toBeInTheDocument(); | |||
// On overall code | |||
await user.click(await screen.findByText('quality_gates.add_condition')); | |||
dialog = within(screen.getByRole('dialog')); | |||
await selectEvent.select(dialog.getByRole('combobox'), ['Info Issues']); | |||
await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' })); | |||
await user.click(dialog.getByLabelText('quality_gates.conditions.operator')); | |||
await selectEvent.select(dialog.byRole('combobox').get(), ['Info Issues']); | |||
await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get()); | |||
await user.click(dialog.byLabelText('quality_gates.conditions.operator').get()); | |||
await user.click(dialog.getByText('quality_gates.operator.LT')); | |||
await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' })); | |||
await user.click(dialog.byText('quality_gates.operator.LT').get()); | |||
await user.click(dialog.byRole('textbox', { name: 'quality_gates.conditions.value' }).get()); | |||
await user.keyboard('42'); | |||
await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' })); | |||
await user.click(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get()); | |||
const overallConditions = within(await screen.findByTestId('quality-gates__conditions-overall')); | |||
const overallConditions = byTestId('quality-gates__conditions-overall'); | |||
expect(await overallConditions.findByRole('cell', { name: 'Info Issues' })).toBeInTheDocument(); | |||
expect(await overallConditions.findByRole('cell', { name: '42' })).toBeInTheDocument(); | |||
expect( | |||
await overallConditions.byRole('cell', { name: 'Info Issues' }).find(), | |||
).toBeInTheDocument(); | |||
expect(await overallConditions.byRole('cell', { name: '42' }).find()).toBeInTheDocument(); | |||
// Select a rating | |||
await user.click(await screen.findByText('quality_gates.add_condition')); | |||
dialog = within(screen.getByRole('dialog')); | |||
await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' })); | |||
await selectEvent.select(dialog.getByRole('combobox'), ['Maintainability Rating']); | |||
await user.click(dialog.getByLabelText('quality_gates.conditions.value')); | |||
await user.click(dialog.getByText('B')); | |||
await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' })); | |||
await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get()); | |||
await selectEvent.select(dialog.byRole('combobox').get(), ['Maintainability Rating']); | |||
await user.click(dialog.byLabelText('quality_gates.conditions.value').get()); | |||
await user.click(dialog.byText('B').get()); | |||
await user.click(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get()); | |||
expect( | |||
await overallConditions.findByRole('cell', { name: 'Maintainability Rating' }), | |||
await overallConditions.byRole('cell', { name: 'Maintainability Rating' }).find(), | |||
).toBeInTheDocument(); | |||
expect(await overallConditions.findByRole('cell', { name: 'B' })).toBeInTheDocument(); | |||
expect(await overallConditions.byRole('cell', { name: 'B' }).find()).toBeInTheDocument(); | |||
}); | |||
it('should be able to edit a condition', async () => { |
@@ -17,12 +17,20 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { MetricKey } from '../types/metrics'; | |||
import { MetricKey, MetricType } from '../types/metrics'; | |||
import { | |||
QualityGateApplicationStatusChildProject, | |||
QualityGateProjectStatus, | |||
QualityGateStatusCondition, | |||
} from '../types/quality-gates'; | |||
import { Metric } from '../types/types'; | |||
import { translate } from './l10n'; | |||
export function getOperatorLabel(op: string, metric: Metric) { | |||
return metric.type === MetricType.Rating | |||
? translate('quality_gates.operator', op, 'rating') | |||
: translate('quality_gates.operator', op); | |||
} | |||
export function extractStatusConditionsFromProjectStatus( | |||
projectStatus: QualityGateProjectStatus, |