You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CustomRuleFormFieldsCCT.tsx 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import {
  21. Checkbox,
  22. FormField,
  23. Highlight,
  24. InputSelect,
  25. LightPrimary,
  26. RequiredIcon,
  27. TextError,
  28. } from 'design-system';
  29. import React from 'react';
  30. import { useIntl } from 'react-intl';
  31. import SoftwareImpactSeverityIcon from '../../../components/icon-mappers/SoftwareImpactSeverityIcon';
  32. import {
  33. CLEAN_CODE_ATTRIBUTES_BY_CATEGORY,
  34. CLEAN_CODE_CATEGORIES,
  35. IMPACT_SEVERITIES,
  36. SOFTWARE_QUALITIES,
  37. } from '../../../helpers/constants';
  38. import {
  39. CleanCodeAttribute,
  40. CleanCodeAttributeCategory,
  41. SoftwareImpact,
  42. SoftwareImpactSeverity,
  43. SoftwareQuality,
  44. } from '../../../types/clean-code-taxonomy';
  45. interface Props<T> {
  46. value: T;
  47. onChange: (value: T) => void;
  48. disabled?: boolean;
  49. }
  50. export function CleanCodeCategoryField(props: Readonly<Props<CleanCodeAttributeCategory>>) {
  51. const { value, disabled } = props;
  52. const intl = useIntl();
  53. const categories = CLEAN_CODE_CATEGORIES.map((category) => ({
  54. value: category,
  55. label: intl.formatMessage({ id: `rule.clean_code_attribute_category.${category}` }),
  56. }));
  57. return (
  58. <FormField
  59. ariaLabel={intl.formatMessage({ id: 'category' })}
  60. label={intl.formatMessage({ id: 'category' })}
  61. htmlFor="coding-rules-custom-clean-code-category"
  62. >
  63. <InputSelect
  64. options={categories}
  65. inputId="coding-rules-custom-clean-code-category"
  66. onChange={(option) => props.onChange(option?.value as CleanCodeAttributeCategory)}
  67. isClearable={false}
  68. isDisabled={disabled}
  69. isSearchable={false}
  70. value={categories.find((category) => category.value === value)}
  71. />
  72. </FormField>
  73. );
  74. }
  75. export function CleanCodeAttributeField(
  76. props: Readonly<Props<CleanCodeAttribute> & { category: CleanCodeAttributeCategory }>,
  77. ) {
  78. const { value, disabled, category, onChange } = props;
  79. const initialAttribute = React.useRef(value);
  80. const intl = useIntl();
  81. const attributes = CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].map((attribute) => ({
  82. value: attribute,
  83. label: intl.formatMessage({ id: `rule.clean_code_attribute.${attribute}` }),
  84. }));
  85. // Set default CC attribute when category changes
  86. React.useEffect(() => {
  87. if (CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].includes(value)) {
  88. return;
  89. }
  90. const initialAttributeIndex = CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].findIndex(
  91. (attribute) => attribute === initialAttribute.current,
  92. );
  93. onChange(
  94. CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category][
  95. initialAttributeIndex === -1 ? 0 : initialAttributeIndex
  96. ],
  97. );
  98. }, [onChange, category, value]);
  99. return (
  100. <FormField
  101. ariaLabel={intl.formatMessage({ id: 'attribute' })}
  102. label={intl.formatMessage({ id: 'attribute' })}
  103. htmlFor="coding-rules-custom-clean-code-attribute"
  104. >
  105. <InputSelect
  106. options={attributes}
  107. inputId="coding-rules-custom-clean-code-attribute"
  108. onChange={(option) => props.onChange(option?.value as CleanCodeAttribute)}
  109. isClearable={false}
  110. isDisabled={disabled}
  111. isSearchable={false}
  112. value={attributes.find((attribute) => attribute.value === value)}
  113. />
  114. </FormField>
  115. );
  116. }
  117. export function SoftwareQualitiesFields(
  118. props: Readonly<Props<SoftwareImpact[]> & { error: boolean }>,
  119. ) {
  120. const { value, disabled, error } = props;
  121. const intl = useIntl();
  122. const severities = React.useMemo(
  123. () =>
  124. IMPACT_SEVERITIES.map((severity) => ({
  125. value: severity,
  126. label: intl.formatMessage({ id: `severity.${severity}` }),
  127. Icon: <SoftwareImpactSeverityIcon severity={severity} />,
  128. })),
  129. [intl],
  130. );
  131. const handleSoftwareQualityChange = (quality: SoftwareQuality, checked: boolean) => {
  132. if (checked) {
  133. props.onChange([
  134. ...value,
  135. { softwareQuality: quality, severity: SoftwareImpactSeverity.Low },
  136. ]);
  137. } else {
  138. props.onChange(value.filter((impact) => impact.softwareQuality !== quality));
  139. }
  140. };
  141. const handleSeverityChange = (quality: SoftwareQuality, severity: SoftwareImpactSeverity) => {
  142. props.onChange(
  143. value.map((impact) =>
  144. impact.softwareQuality === quality ? { ...impact, severity } : impact,
  145. ),
  146. );
  147. };
  148. return (
  149. <fieldset className="sw-mt-2 sw-mb-4 sw-relative">
  150. <legend className="sw-w-full sw-flex sw-justify-between sw-gap-6 sw-mb-4">
  151. <Highlight className="sw-w-full">
  152. {intl.formatMessage({ id: 'software_quality' })}
  153. <RequiredIcon aria-label={intl.formatMessage({ id: 'required' })} className="sw-ml-1" />
  154. </Highlight>
  155. <Highlight className="sw-w-full">
  156. {intl.formatMessage({ id: 'severity' })}
  157. <RequiredIcon aria-label={intl.formatMessage({ id: 'required' })} className="sw-ml-1" />
  158. </Highlight>
  159. </legend>
  160. {SOFTWARE_QUALITIES.map((quality) => {
  161. const selectedQuality = value.find((impact) => impact.softwareQuality === quality);
  162. const selectedSeverity = selectedQuality
  163. ? severities.find((severity) => severity.value === selectedQuality.severity)
  164. : null;
  165. return (
  166. <fieldset key={quality} className="sw-flex sw-justify-between sw-gap-6 sw-mb-4">
  167. <legend className="sw-sr-only">
  168. {intl.formatMessage(
  169. { id: 'coding_rules.custom_rule.software_quality_x' },
  170. { quality },
  171. )}
  172. </legend>
  173. <Checkbox
  174. className="sw-w-full"
  175. checked={Boolean(selectedQuality)}
  176. onCheck={(checked) => {
  177. handleSoftwareQualityChange(quality, checked);
  178. }}
  179. label={quality}
  180. >
  181. <LightPrimary className="sw-ml-3">
  182. {intl.formatMessage({ id: `software_quality.${quality}` })}
  183. </LightPrimary>
  184. </Checkbox>
  185. <InputSelect
  186. aria-label={intl.formatMessage({ id: 'severity' })}
  187. className="sw-w-full"
  188. options={severities}
  189. placeholder={intl.formatMessage({ id: 'none' })}
  190. onChange={(option) =>
  191. handleSeverityChange(quality, option?.value as SoftwareImpactSeverity)
  192. }
  193. isClearable={false}
  194. isDisabled={disabled || !selectedQuality}
  195. isSearchable={false}
  196. value={selectedSeverity}
  197. />
  198. </fieldset>
  199. );
  200. })}
  201. {error && (
  202. <TextError
  203. className="sw-font-regular sw-absolute sw--bottom-3"
  204. text={intl.formatMessage({ id: 'coding_rules.custom_rule.select_software_quality' })}
  205. />
  206. )}
  207. </fieldset>
  208. );
  209. }