* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { HttpStatusCode } from 'axios';
import { cloneDeep, countBy, pick, trim } from 'lodash';
import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
import { getStandards } from '../../helpers/security-standard';
import { RuleRepository, SearchRulesResponse } from '../../types/coding-rules';
import { ComponentQualifier, Visibility } from '../../types/component';
import { RawIssuesResponse } from '../../types/issues';
-import { SearchRulesQuery } from '../../types/rules';
+import { RuleStatus, SearchRulesQuery } from '../../types/rules';
import { SecurityStandard } from '../../types/security';
import { Dict, Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../types/types';
import { NoticeType } from '../../types/users';
cwe,
activation,
}: FacetFilter) {
- let filteredRules = this.rules;
+ let filteredRules = this.getRulesFilteredByRemovedStatus();
if (cleanCodeAttributeCategories) {
filteredRules = filteredRules.filter(
(r) =>
this.rulesActivations = mockRulesActivationsInQP();
}
+ getRulesFilteredByRemovedStatus() {
+ return this.rules.filter((r) => r.status !== RuleStatus.Removed);
+ }
+
allRulesCount() {
- return this.rules.length;
+ return this.getRulesFilteredByRemovedStatus().length;
}
allRulesName() {
- return this.rules.map((r) => r.name);
+ return this.getRulesFilteredByRemovedStatus().map((r) => r.name);
}
allQualityProfile(language: string) {
};
handleUpdateRule = (data: RulesUpdateRequest): Promise<RuleDetails> => {
- const rule = this.rules.find((r) => r.key === data.key);
+ // find rule if key is in the list of rules or key is a part of 'repo:key'
+ const rule = this.rules.find((r) => data.key.split(':').some((part) => part === r.key));
if (rule === undefined) {
return Promise.reject({
errors: [{ msg: `No rule has been found for id ${data.key}` }],
});
}
+
const template = this.rules.find((r) => r.key === rule.templateKey);
// Lets not convert the md to html in test.
rule.mdNote = data.markdown_note !== undefined ? data.markdown_note : rule.mdNote;
rule.htmlNote = data.markdown_note !== undefined ? data.markdown_note : rule.htmlNote;
rule.name = data.name !== undefined ? data.name : rule.name;
+ rule.status = rule.status === RuleStatus.Removed ? RuleStatus.Ready : rule.status;
if (template && data.params) {
rule.params = [];
data.params.split(';').forEach((param) => {
}) ?? [],
});
+ const rulesFromTemplateWithSameKeys = this.rules.filter(
+ (rule) => rule.templateKey === newRule.templateKey && rule.key === newRule.key,
+ );
+
+ if (rulesFromTemplateWithSameKeys.length > 0) {
+ if (
+ rulesFromTemplateWithSameKeys.find(
+ (rule) => rule.status === RuleStatus.Removed && rule.key === newRule.key,
+ )
+ ) {
+ return Promise.reject({
+ status: HttpStatusCode.Conflict,
+ errors: [{ msg: `Rule with the same was removed before` }],
+ });
+ }
+
+ return Promise.reject({
+ errors: [{ msg: `A rule with key ${newRule.key} already exists` }],
+ });
+ }
+
this.rules.push(newRule);
return this.reply(newRule);
activation,
});
}
+
const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
return this.reply({
actives: qprofile ? this.rulesActivations : undefined,
handleBulkActivateRules = () => {
if (this.applyWithWarning) {
return this.reply({
- succeeded: this.rules.length - 1,
+ succeeded: this.getRulesFilteredByRemovedStatus().length - 1,
failed: 1,
errors: [{ msg: 'c rule c:S6069 cannot be activated on cpp profile SonarSource' }],
});
}
return this.reply({
- succeeded: this.rules.length,
+ succeeded: this.getRulesFilteredByRemovedStatus().length,
failed: 0,
errors: [],
});
handleBulkDeactivateRules = () => {
return this.reply({
- succeeded: this.rules.length,
+ succeeded: this.getRulesFilteredByRemovedStatus().length,
failed: 0,
});
};
export const RULE_9 = 'rule9';
export const RULE_10 = 'rule10';
export const RULE_11 = 'rule11';
+export const RULE_12 = 'rule12';
// Quality Profiles.
export const QP_1 = 'p1';
SoftwareImpactSeverity,
SoftwareQuality,
} from '../../../types/clean-code-taxonomy';
+import { RuleStatus } from '../../../types/rules';
import {
ADVANCED_RULE,
QP_1,
RULE_1,
RULE_10,
RULE_11,
+ RULE_12,
RULE_2,
RULE_3,
RULE_4,
{ key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' },
{ key: '2', type: 'NUMBER', defaultValue: 'default value for key 2' },
],
- templateKey: 'rule8',
+ templateKey: RULE_8,
}),
mockRuleDetails({
createdAt: '2022-12-16T17:26:54+0100',
langName: 'Java',
name: 'Common java rule',
}),
+ mockRuleDetails({
+ key: RULE_12,
+ type: 'BUG',
+ severity: 'MINOR',
+ lang: 'py',
+ langName: 'Python',
+ tags: ['awesome'],
+ name: 'Deleted custom rule based on rule8',
+ templateKey: RULE_8,
+ status: RuleStatus.Removed,
+ }),
];
}
import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock';
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import { mockLoggedInUser } from '../../../helpers/testMocks';
+import { byText } from '../../../helpers/testSelector';
import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
import { getPageObjects, renderCodingRulesApp } from '../utils-tests';
expect(ui.customRuleItemLink('New Custom Rule').get()).toBeInTheDocument();
});
+ it('can reactivate custom rule', async () => {
+ const { ui, user } = getPageObjects();
+ rulesHandler.setIsAdmin();
+ renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
+ await ui.detailsloaded();
+
+ // Try create custom rule with existing rule with removed status
+ await user.click(ui.createCustomRuleButton.get());
+ await user.type(ui.ruleNameTextbox.get(), 'Reactivate custom Rule');
+ await user.clear(ui.keyTextbox.get());
+ await user.type(ui.keyTextbox.get(), 'rule12');
+ await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
+
+ await user.click(ui.createButton.get());
+
+ expect(byText('coding_rules.reactivate.help').get()).toBeInTheDocument();
+
+ // Reactivate rule
+ await user.click(ui.reactivateButton.get());
+ // Verify the rule is reactivated
+ expect(ui.customRuleItemLink('Reactivate custom Rule').get()).toBeInTheDocument();
+ });
+
it('can edit custom rule', async () => {
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
templateRule.cleanCodeAttribute ?? CleanCodeAttribute.Conventional,
);
const [impacts, setImpacts] = React.useState<SoftwareImpact[]>(templateRule?.impacts ?? []);
- const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(props.onClose);
+ const customRulesSearchParams = {
+ f: 'name,severity,params',
+ template_key: templateRule.key,
+ };
+ const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(
+ customRulesSearchParams,
+ props.onClose,
+ );
const { mutate: createRule, isLoading: creatingRule } = useCreateRuleMutation(
- {
- f: 'name,severity,params',
- template_key: templateRule.key,
- },
+ customRulesSearchParams,
props.onClose,
(response: Response) => {
setReactivating(response.status === HttpStatusCode.Conflict);
},
);
+ const warningRef = React.useRef<HTMLDivElement>(null);
const submitting = updatingRule || creatingRule;
const hasError = impacts.length === 0;
params: stringifiedParams,
status,
};
- return customRule
- ? updateRule({ ...ruleData, key: customRule.key })
- : createRule({
- ...ruleData,
- customKey: key,
- preventReactivation: !reactivating,
- templateKey: templateRule.key,
- cleanCodeAttribute: ccAttribute,
- impacts,
- });
+
+ if (customRule) {
+ updateRule({
+ ...ruleData,
+ key: customRule.key,
+ });
+ } else if (reactivating) {
+ updateRule({
+ ...ruleData,
+ key: `${templateRule.repo}:${key}`,
+ });
+ } else {
+ createRule({
+ ...ruleData,
+ customKey: key,
+ templateKey: templateRule.key,
+ preventReactivation: true,
+ cleanCodeAttribute: ccAttribute,
+ impacts,
+ });
+ }
};
+ // If key changes, then most likely user did it to create a new rule instead of reactivating one
+ React.useEffect(() => {
+ setReactivating(false);
+ }, [key]);
+
+ // scroll to warning when it appears
+ React.useEffect(() => {
+ if (reactivating) {
+ warningRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, [reactivating]);
+
const NameField = React.useMemo(
() => (
<FormField
}}
>
{reactivating && (
- <FlagMessage variant="warning" className="sw-mb-6">
- {translate('coding_rules.reactivate.help')}
- </FlagMessage>
+ <div ref={warningRef}>
+ <FlagMessage variant="warning" className="sw-mb-6">
+ {translate('coding_rules.reactivate.help')}
+ </FlagMessage>
+ </div>
)}
<MandatoryFieldsExplanation className="sw-mb-4" />
const [descriptionForm, setDescriptionForm] = React.useState(false);
const [removeDescriptionModal, setDescriptionModal] = React.useState(false);
- const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(() =>
+ const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(undefined, () =>
setDescriptionForm(false),
);
statusSelect: byRole('combobox', { name: 'coding_rules.filters.status' }),
descriptionTextbox: byRole('textbox', { name: 'description' }),
createButton: byRole('button', { name: 'create' }),
+ reactivateButton: byRole('button', { name: 'coding_rules.reactivate' }),
deleteButton: byRole('button', { name: 'delete' }),
};
});
}
-export function useUpdateRuleMutation(onSuccess?: (rule: RuleDetails) => unknown) {
+export function useUpdateRuleMutation(
+ searchQuery?: SearchRulesQuery,
+ onSuccess?: (rule: RuleDetails) => unknown,
+) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateRule,
onSuccess: (rule) => {
onSuccess?.(rule);
+ queryClient.setQueryData<SearchRulesResponse>(
+ getRulesQueryKey('search', searchQuery),
+ (oldData) => {
+ return oldData ? { ...oldData, rules: [rule, ...oldData.rules] } : undefined;
+ },
+ );
queryClient.setQueryData<{ actives?: RuleActivation[]; rule: RuleDetails }>(
getRulesQueryKey('details', rule.key),
(oldData) => {