From 57cc18843e7f028cbf8c1149b02407e15407d6fa Mon Sep 17 00:00:00 2001 From: Kevin Silva Date: Wed, 27 Sep 2023 17:45:23 +0200 Subject: [PATCH] SONAR-20500 Rules list header --- .../{TagsSelector.tsx => MultiSelector.tsx} | 27 ++-- .../src/components/__tests__/Tags-test.tsx | 8 +- .../design-system/src/components/index.ts | 2 +- .../src/components/input/MultiSelectMenu.tsx | 47 ++++--- .../coding-rules/__tests__/CodingRules-it.ts | 12 +- .../coding-rules/components/BulkChange.tsx | 81 ++++++----- .../components/BulkChangeModal.tsx | 130 +++++++++--------- .../components/CodingRulesApp.tsx | 13 ++ .../coding-rules/components/PageActions.tsx | 13 +- .../components/QualityProfileSelector.tsx | 114 +++++++++++++++ .../components/RuleDetailsTagsPopup.tsx | 8 +- .../main/js/apps/coding-rules/utils-tests.tsx | 4 +- .../js/apps/issues/components/TagsSelect.tsx | 8 +- .../about/components/MetaTags.tsx | 8 +- .../main/js/components/common/PageCounter.tsx | 10 +- .../__snapshots__/PageCounter-test.tsx.snap | 6 +- .../issue/popups/IssueTagsPopup.tsx | 8 +- .../resources/org/sonar/l10n/core.properties | 7 +- 18 files changed, 319 insertions(+), 187 deletions(-) rename server/sonar-web/design-system/src/components/{TagsSelector.tsx => MultiSelector.tsx} (78%) create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/QualityProfileSelector.tsx diff --git a/server/sonar-web/design-system/src/components/TagsSelector.tsx b/server/sonar-web/design-system/src/components/MultiSelector.tsx similarity index 78% rename from server/sonar-web/design-system/src/components/TagsSelector.tsx rename to server/sonar-web/design-system/src/components/MultiSelector.tsx index 55eaac13c04..dede9a7a443 100644 --- a/server/sonar-web/design-system/src/components/TagsSelector.tsx +++ b/server/sonar-web/design-system/src/components/MultiSelector.tsx @@ -21,50 +21,55 @@ 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; + onSearch?: (query: string) => Promise; onSelect: (item: string) => void; onUnselect: (item: string) => void; searchInputAriaLabel: string; - selectedTags: string[]; - tags: string[]; + selectedElements: string[]; } const LIST_SIZE = 10; -export function TagsSelector(props: Props) { +export function MultiSelector(props: Readonly) { const { allowNewElements, createElementLabel, headerLabel, noResultsLabel, searchInputAriaLabel, - selectedTags, - tags, + selectedElements, + elements, + allowSearch = true, + listSize = LIST_SIZE, } = props; return ( {headerLabel}} - listSize={LIST_SIZE} + listSize={listSize} noResultsLabel={noResultsLabel} onSearch={props.onSearch} onSelect={props.onSelect} onUnselect={props.onUnselect} placeholder={searchInputAriaLabel} searchInputAriaLabel={searchInputAriaLabel} - selectedElements={selectedTags} - validateSearchInput={validateTag} + selectedElements={selectedElements} + validateSearchInput={validateElement} /> ); } -export function validateTag(value: string) { +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/__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 ( -