From: Kevin Silva Date: Wed, 27 Sep 2023 15:45:23 +0000 (+0200) Subject: SONAR-20500 Rules list header X-Git-Tag: 10.3.0.82913~268 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=57cc18843e7f028cbf8c1149b02407e15407d6fa;p=sonarqube.git SONAR-20500 Rules list header --- diff --git a/server/sonar-web/design-system/src/components/MultiSelector.tsx b/server/sonar-web/design-system/src/components/MultiSelector.tsx new file mode 100644 index 00000000000..dede9a7a443 --- /dev/null +++ b/server/sonar-web/design-system/src/components/MultiSelector.tsx @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 { MultiSelectMenu } from './input/MultiSelectMenu'; + +interface Props { + allowNewElements?: boolean; + allowSearch?: boolean; + createElementLabel: string; + elements: string[]; + headerLabel: string; + listSize?: number; + noResultsLabel: string; + onSearch?: (query: string) => Promise; + onSelect: (item: string) => void; + onUnselect: (item: string) => void; + searchInputAriaLabel: string; + selectedElements: string[]; +} + +const LIST_SIZE = 10; + +export function MultiSelector(props: Readonly) { + const { + allowNewElements, + createElementLabel, + headerLabel, + noResultsLabel, + searchInputAriaLabel, + selectedElements, + elements, + allowSearch = true, + listSize = LIST_SIZE, + } = props; + + return ( + {headerLabel}} + listSize={listSize} + noResultsLabel={noResultsLabel} + onSearch={props.onSearch} + onSelect={props.onSelect} + onUnselect={props.onUnselect} + placeholder={searchInputAriaLabel} + searchInputAriaLabel={searchInputAriaLabel} + selectedElements={selectedElements} + validateSearchInput={validateElement} + /> + ); +} + +export function validateElement(value: string) { + // Allow only a-z, 0-9, '+', '-', '#', '.' + return value.toLowerCase().replace(/[^-a-z0-9+#.]/gi, ''); +} diff --git a/server/sonar-web/design-system/src/components/TagsSelector.tsx b/server/sonar-web/design-system/src/components/TagsSelector.tsx deleted file mode 100644 index 55eaac13c04..00000000000 --- a/server/sonar-web/design-system/src/components/TagsSelector.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 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 { MultiSelectMenu } from './input/MultiSelectMenu'; - -interface Props { - allowNewElements?: boolean; - createElementLabel: string; - headerLabel: string; - noResultsLabel: string; - onSearch: (query: string) => Promise; - onSelect: (item: string) => void; - onUnselect: (item: string) => void; - searchInputAriaLabel: string; - selectedTags: string[]; - tags: string[]; -} - -const LIST_SIZE = 10; - -export function TagsSelector(props: Props) { - const { - allowNewElements, - createElementLabel, - headerLabel, - noResultsLabel, - searchInputAriaLabel, - selectedTags, - tags, - } = props; - - return ( - {headerLabel}} - listSize={LIST_SIZE} - noResultsLabel={noResultsLabel} - onSearch={props.onSearch} - onSelect={props.onSelect} - onUnselect={props.onUnselect} - placeholder={searchInputAriaLabel} - searchInputAriaLabel={searchInputAriaLabel} - selectedElements={selectedTags} - validateSearchInput={validateTag} - /> - ); -} - -export function validateTag(value: string) { - // Allow only a-z, 0-9, '+', '-', '#', '.' - return value.toLowerCase().replace(/[^-a-z0-9+#.]/gi, ''); -} diff --git a/server/sonar-web/design-system/src/components/__tests__/Tags-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Tags-test.tsx index 73a623cee34..c6082251dbc 100644 --- a/server/sonar-web/design-system/src/components/__tests__/Tags-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/Tags-test.tsx @@ -25,8 +25,8 @@ import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { renderWithContext } from '../../helpers/testUtils'; import { FCProps } from '../../types/misc'; +import { MultiSelector } from '../MultiSelector'; import { Tags } from '../Tags'; -import { TagsSelector } from '../TagsSelector'; it('should display "no tags"', () => { renderTags({ tags: [] }); @@ -83,8 +83,9 @@ function Wrapper(overrides: Partial> = {}) { const [selectedTags, setSelectedTags] = useState(overrides.tags ?? ['tag1']); const overlay = ( - > = {}) { } }} searchInputAriaLabel="search" - selectedTags={selectedTags} - tags={['tag1', 'tag2', 'tag3']} + selectedElements={selectedTags} /> ); diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 34f72bf173b..16e93fbc755 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -56,6 +56,7 @@ export * from './MainAppBar'; export * from './MainMenu'; export * from './MainMenuItem'; export * from './MetricsRatingBadge'; +export * from './MultiSelector'; export * from './NavBarTabs'; export * from './NewCodeLegend'; export * from './OutsideClickHandler'; @@ -71,7 +72,6 @@ export { Spinner } from './Spinner'; export * from './SpotlightTour'; export * from './Table'; export * from './Tags'; -export * from './TagsSelector'; export * from './Text'; export * from './Title'; export { ToggleButton } from './ToggleButton'; diff --git a/server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx b/server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx index bd73d4672ec..0a85dd0c093 100644 --- a/server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx +++ b/server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx @@ -27,6 +27,7 @@ import { MultiSelectMenuOption } from './MultiSelectMenuOption'; interface Props { allowNewElements?: boolean; + allowSearch?: boolean; allowSelection?: boolean; createElementLabel: string; elements: string[]; @@ -35,7 +36,7 @@ interface Props { inputId?: string; listSize: number; noResultsLabel: string; - onSearch: (query: string) => Promise; + onSearch?: (query: string) => Promise; onSelect: (item: string) => void; onUnselect: (item: string) => void; placeholder: string; @@ -165,8 +166,10 @@ export class MultiSelectMenu extends PureComponent { }; onSearchQuery = (query: string) => { - this.setState({ activeIdx: 0, loading: true, query }); - this.props.onSearch(query).then(this.stopLoading, this.stopLoading); + if (this.props.onSearch) { + this.setState({ activeIdx: 0, loading: true, query }); + this.props.onSearch(query).then(this.stopLoading, this.stopLoading); + } }; onSelectItem = (item: string) => { @@ -205,7 +208,7 @@ export class MultiSelectMenu extends PureComponent { return { unselectedElements: difference(this.props.elements, this.props.selectedElements).slice( 0, - listSize - state.selectedElements.length + listSize - state.selectedElements.length, ), }; }); @@ -255,6 +258,7 @@ export class MultiSelectMenu extends PureComponent { render() { const { + allowSearch = true, allowSelection = true, allowNewElements = true, createElementLabel, @@ -274,22 +278,27 @@ export class MultiSelectMenu extends PureComponent { return (
(this.container = div)}> -
- -
- {headerNode} + {allowSearch && ( + <> +
+ +
+ {headerNode} + + )}
    diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index f4e273180e6..62791902310 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -256,8 +256,7 @@ describe('Rules app list', () => { expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11); }); - // eslint-disable-next-line jest/no-disabled-tests - it.skip('filters by search', async () => { + it('filters by search', async () => { const { ui, user } = getPageObjects(); renderCodingRulesApp(mockCurrentUser()); await ui.appLoaded(); @@ -288,11 +287,12 @@ describe('Rules app list', () => { await user.click(ui.bulkChangeButton.get()); await user.click(ui.activateIn.get()); - const dialog = ui.bulkChangeDialog(1).get(); - expect(dialog).toBeInTheDocument(); + const dialog = ui.bulkChangeDialog(1); + expect(dialog.get()).toBeInTheDocument(); - selectEvent.openMenu(ui.activateInSelect.get()); - expect(ui.noQualityProfiles.get(dialog)).toBeInTheDocument(); + await user.click(ui.activateInSelect.get()); + + expect(ui.noQualityProfiles.get(dialog.get())).toBeInTheDocument(); }); it('should be able to bulk activate quality profile', async () => { diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx index 9d8b36aed71..bf8ff0c4caa 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx @@ -17,12 +17,18 @@ * 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, + ButtonSecondary, + ChevronDownIcon, + Dropdown, + ItemButton, + PopupPlacement, + PopupZLevel, +} from 'design-system'; import * as React from 'react'; import { Profile } from '../../../api/quality-profiles'; -import Dropdown from '../../../components/controls/Dropdown'; import Tooltip from '../../../components/controls/Tooltip'; -import { Button } from '../../../components/controls/buttons'; -import { PopupPlacement } from '../../../components/ui/popups'; import { translate } from '../../../helpers/l10n'; import { Dict } from '../../../types/types'; import { Query } from '../query'; @@ -50,27 +56,19 @@ export default class BulkChange extends React.PureComponent { closeModal = () => this.setState({ action: undefined, modal: false, profile: undefined }); - handleActivateClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.currentTarget.blur(); + handleActivateClick = () => { this.setState({ action: 'activate', modal: true, profile: undefined }); }; - handleActivateInProfileClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.currentTarget.blur(); + handleActivateInProfileClick = () => { this.setState({ action: 'activate', modal: true, profile: this.getSelectedProfile() }); }; - handleDeactivateClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.currentTarget.blur(); + handleDeactivateClick = () => { this.setState({ action: 'deactivate', modal: true, profile: undefined }); }; - handleDeactivateInProfileClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.currentTarget.blur(); + handleDeactivateInProfileClick = () => { this.setState({ action: 'deactivate', modal: true, profile: this.getSelectedProfile() }); }; @@ -82,9 +80,7 @@ export default class BulkChange extends React.PureComponent { if (!canBulkChange) { return ( - + {translate('bulk_change')} ); } @@ -100,37 +96,38 @@ export default class BulkChange extends React.PureComponent { return ( <> -
  • - - {translate('coding_rules.activate_in')}… - -
  • + <> + + {translate('coding_rules.activate_in')} + + {allowActivateOnProfile && profile && ( -
  • - - {translate('coding_rules.activate_in')} {profile.name} - -
  • + + {translate('coding_rules.activate_in')} {profile.name} + )} -
  • - - {translate('coding_rules.deactivate_in')}… - -
  • + + + {translate('coding_rules.deactivate_in')} + + {allowDeactivateOnProfile && profile && ( -
  • - - {translate('coding_rules.deactivate_in')} {profile.name} - -
  • + + {translate('coding_rules.deactivate_in')} {profile.name} + )} -
+ } > - + + {translate('bulk_change')} + + {this.state.modal && this.state.action && ( { this.state = { finished: false, - modalWrapperNode: null, results: [], selectedProfiles, submitting: false, @@ -85,10 +81,6 @@ export class BulkChangeModal extends React.PureComponent { this.mounted = false; } - setModalWrapperNode = (node: HTMLDivElement | null) => { - this.setState({ modalWrapperNode: node }); - }; - handleProfileSelect = (selectedProfiles: Profile[]) => { this.setState({ selectedProfiles }); }; @@ -168,7 +160,11 @@ export class BulkChangeModal extends React.PureComponent { ? languages[profile.language].name : profile.language; return ( - + {result.failed ? translateWithParameters( 'coding_rules.bulk_change.warning', @@ -183,27 +179,20 @@ export class BulkChangeModal extends React.PureComponent { language, result.succeeded, )} - + ); }; renderProfileSelect = () => { const profiles = this.getAvailableQualityProfiles(); + const { selectedProfiles } = this.state; return ( -