From 5e2d38f17ef22ea4e983938532bb05108c598036 Mon Sep 17 00:00:00 2001 From: Kevin Silva Date: Fri, 2 Jun 2023 10:28:13 +0200 Subject: [PATCH] SONAR-19345 Update box contents for each issue --- .../design-system/src/components/Checkbox.tsx | 6 +- .../src/components/ColorsLegend.tsx | 2 +- .../src/components/DiscreetSelect.tsx | 12 +- .../design-system/src/components/Dropdown.tsx | 10 +- .../src/components/InputSelect.tsx | 4 +- .../src/components/SearchSelectDropdown.tsx | 7 + .../design-system/src/components/Tags.tsx | 37 ++-- .../design-system/src/components/index.ts | 1 + .../js/apps/issues/__tests__/IssuesApp-it.tsx | 197 ++++-------------- .../components/ComponentBreadcrumbs.tsx | 30 ++- .../js/apps/issues/components/IssueHeader.tsx | 2 +- .../js/apps/issues/components/IssuesApp.tsx | 9 +- .../js/apps/issues/components/IssuesList.tsx | 4 +- .../js/apps/issues/components/ListItem.tsx | 27 ++- .../js/apps/issues/components/PageActions.tsx | 20 +- .../js/apps/issues/components/TotalEffort.tsx | 14 +- .../src/main/js/apps/issues/test-utils.tsx | 9 + .../components/SourceViewer/SourceViewer.tsx | 4 - .../SourceViewer/SourceViewerCode.tsx | 4 - .../__tests__/SourceViewer-it.tsx | 35 ++-- .../__snapshots__/SourceViewer-test.tsx.snap | 2 - .../components/LineIssuesList.tsx | 6 +- .../src/main/js/components/issue/Issue.tsx | 7 - .../components/issue/__tests__/Issue-it.tsx | 189 ++++------------- .../issue/components/IssueActionsBar.tsx | 161 +++++++++----- .../issue/components/IssueAssign.tsx | 175 ++++++++++------ .../issue/components/IssueCommentAction.tsx | 25 +-- .../issue/components/IssueMessage.tsx | 32 ++- .../issue/components/IssueSeverity.tsx | 69 +++--- .../components/issue/components/IssueTags.tsx | 51 ++--- .../issue/components/IssueTitleBar.tsx | 123 +++-------- .../issue/components/IssueTransition.tsx | 90 ++++---- .../components/issue/components/IssueType.tsx | 65 ++---- .../components/issue/components/IssueView.tsx | 95 +++------ .../issue/components/SimilarIssuesFilter.tsx | 74 ------- .../issue/popups/IssueTagsPopup.tsx | 71 +++++++ .../issue/popups/SetAssigneePopup.tsx | 129 ------------ .../issue/popups/SetIssueTagsPopup.tsx | 89 -------- .../issue/popups/SetSeverityPopup.tsx | 48 ----- .../issue/popups/SetTransitionPopup.tsx | 66 ------ .../components/issue/popups/SetTypePopup.tsx | 48 ----- .../issue/popups/SimilarIssuesPopup.tsx | 138 ------------ .../resources/org/sonar/l10n/core.properties | 7 +- 43 files changed, 717 insertions(+), 1477 deletions(-) delete mode 100644 server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.tsx create mode 100644 server/sonar-web/src/main/js/components/issue/popups/IssueTagsPopup.tsx delete mode 100644 server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.tsx delete mode 100644 server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx delete mode 100644 server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.tsx delete mode 100644 server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.tsx delete mode 100644 server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.tsx delete mode 100644 server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.tsx diff --git a/server/sonar-web/design-system/src/components/Checkbox.tsx b/server/sonar-web/design-system/src/components/Checkbox.tsx index 829e7eb181d..6b4a77a3c98 100644 --- a/server/sonar-web/design-system/src/components/Checkbox.tsx +++ b/server/sonar-web/design-system/src/components/Checkbox.tsx @@ -27,12 +27,12 @@ import { CheckIcon } from './icons/CheckIcon'; import { CustomIcon } from './icons/Icon'; interface Props { - ariaLabel?: string; checked: boolean; children?: React.ReactNode; className?: string; disabled?: boolean; id?: string; + label?: string; loading?: boolean; onCheck: (checked: boolean, id?: string) => void; onClick?: (event: React.MouseEvent) => void; @@ -43,12 +43,12 @@ interface Props { } export function Checkbox({ - ariaLabel, checked, disabled, children, className, id, + label, loading = false, onCheck, onFocus, @@ -67,7 +67,7 @@ export function Checkbox({ {right && children}
{ props.onColorClick(color); }} diff --git a/server/sonar-web/design-system/src/components/DiscreetSelect.tsx b/server/sonar-web/design-system/src/components/DiscreetSelect.tsx index dc3fb0c0b4b..b78deb33a91 100644 --- a/server/sonar-web/design-system/src/components/DiscreetSelect.tsx +++ b/server/sonar-web/design-system/src/components/DiscreetSelect.tsx @@ -25,8 +25,13 @@ import { InputSelect, LabelValueSelectOption } from './InputSelect'; interface Props { className?: string; + components?: any; customValue?: JSX.Element; - options: LabelValueSelectOption[]; + isDisabled?: boolean; + menuIsOpen?: boolean; + onMenuClose?: () => void; + onMenuOpen?: () => void; + options: Array>; setValue: ({ value }: LabelValueSelectOption) => void; size?: InputSizeKeys; value: V; @@ -35,6 +40,7 @@ interface Props { export function DiscreetSelect({ className, customValue, + onMenuOpen, options, size = 'small', setValue, @@ -45,6 +51,7 @@ export function DiscreetSelect({ { state: State = { open: false }; - componentDidUpdate(_: Props, prevState: State) { + componentDidUpdate(props: Props, prevState: State) { if (!prevState.open && this.state.open && this.props.onOpen) { this.props.onOpen(); } + if (props.openDropdown !== this.props.openDropdown && this.props.openDropdown) { + this.setState({ open: this.props.openDropdown }); + } } handleClose = () => { this.setState({ open: false }); + if (this.props.onClose) { + this.props.onClose(); + } }; handleToggleClick: OnClickCallback = (event) => { diff --git a/server/sonar-web/design-system/src/components/InputSelect.tsx b/server/sonar-web/design-system/src/components/InputSelect.tsx index 1498b2edca8..bac391c1716 100644 --- a/server/sonar-web/design-system/src/components/InputSelect.tsx +++ b/server/sonar-web/design-system/src/components/InputSelect.tsx @@ -120,6 +120,7 @@ export function InputSelect< classNames={{ container: () => 'sw-relative sw-inline-block sw-align-middle', placeholder: () => 'sw-truncate sw-leading-4', + menu: () => 'sw-z-dropdown-menu', menuList: () => 'sw-overflow-y-auto sw-py-2 sw-max-h-[12.25rem]', control: ({ isDisabled }) => classNames( @@ -135,13 +136,14 @@ export function InputSelect< ...props.classNames, }} components={{ - ...props.components, Option: IconOption, SingleValue, IndicatorsContainer, IndicatorSeparator: null, + ...props.components, }} isSearchable={props.isSearchable ?? false} + onMenuOpen={props.onMenuOpen} styles={selectStyle({ size })} /> ); diff --git a/server/sonar-web/design-system/src/components/SearchSelectDropdown.tsx b/server/sonar-web/design-system/src/components/SearchSelectDropdown.tsx index 8212eefd90c..18e724c1ed2 100644 --- a/server/sonar-web/design-system/src/components/SearchSelectDropdown.tsx +++ b/server/sonar-web/design-system/src/components/SearchSelectDropdown.tsx @@ -76,11 +76,18 @@ export function SearchSelectDropdown< isDisabled, minLength, controlAriaLabel, + menuIsOpen, ...rest } = props; const [open, setOpen] = React.useState(false); const [inputValue, setInputValue] = React.useState(''); + React.useEffect(() => { + if (menuIsOpen) { + setOpen(true); + } + }, [menuIsOpen]); + const ref = React.useRef>(null); const toggleDropdown = React.useCallback( diff --git a/server/sonar-web/design-system/src/components/Tags.tsx b/server/sonar-web/design-system/src/components/Tags.tsx index 6e4d4491520..fa3dcfc7748 100644 --- a/server/sonar-web/design-system/src/components/Tags.tsx +++ b/server/sonar-web/design-system/src/components/Tags.tsx @@ -33,10 +33,13 @@ interface Props { className?: string; emptyText: string; menuId?: string; + onClose?: VoidFunction; + open?: boolean; overlay?: React.ReactNode; popupPlacement?: PopupPlacement; tags: string[]; tagsToDisplay?: number; + tooltip?: React.ComponentType<{ overlay: React.ReactNode }>; } export function Tags({ @@ -49,23 +52,29 @@ export function Tags({ popupPlacement, tags, tagsToDisplay = 3, + tooltip, + open, + onClose, }: Props) { const displayedTags = tags.slice(0, tagsToDisplay); const extraTags = tags.slice(tagsToDisplay); + const Tooltip = tooltip || React.Fragment; - const displayedTagsContent = () => ( - - {/* Display first 3 (tagsToDisplay) tags */} - {displayedTags.map((tag) => ( - {tag} - ))} + const displayedTagsContent = (open = false) => ( + + + {/* Display first 3 (tagsToDisplay) tags */} + {displayedTags.map((tag) => ( + {tag} + ))} - {/* Show ellipsis if there are more tags */} - {extraTags.length > 0 ? ... : null} + {/* Show ellipsis if there are more tags */} + {extraTags.length > 0 ? ... : null} - {/* Handle no tags with its own styling */} - {tags.length === 0 && {emptyText}} - + {/* Handle no tags with its own styling */} + {tags.length === 0 && {emptyText}} + + ); return ( @@ -78,17 +87,19 @@ export function Tags({ allowResizing closeOnClick={false} id={menuId} + onClose={onClose} + openDropdown={open} overlay={overlay} placement={popupPlacement} zLevel={PopupZLevel.Global} > - {({ a11yAttrs, onToggleClick }) => ( + {({ a11yAttrs, onToggleClick, open }) => ( - {displayedTagsContent()} + {displayedTagsContent(open)} + )} diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 9cd9b190184..5457b363900 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -68,6 +68,7 @@ export * from './NewCodeLegend'; export * from './OutsideClickHandler'; export { QualityGateIndicator } from './QualityGateIndicator'; export * from './RadioButton'; +export * from './SearchHighlighter'; export * from './SearchSelect'; export * from './SearchSelectDropdown'; export * from './SelectionCard'; diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index a0282be1eea..4d8b43c28e3 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -118,7 +118,7 @@ describe('issues app', () => { renderIssueApp(); // Select an issue with an advanced rule - await user.click(await screen.findByRole('region', { name: 'Fix that' })); + await user.click(await screen.findByRole('link', { name: 'Fix that' })); expect(screen.getByRole('tab', { name: 'issue.tabs.code' })).toBeInTheDocument(); // Are rule headers present? @@ -211,7 +211,7 @@ describe('issues app', () => { await user.click(await ui.issueItem5.find()); expect(ui.projectIssueItem6.getAll()).toHaveLength(2); // there will be 2 buttons one in concise issue and other in code viewer - await user.click(ui.projectIssueItem6.getAll()[1]); + await user.click(ui.issueItemAction6.get()); expect(screen.getByRole('heading', { level: 1, name: 'Second issue' })).toBeInTheDocument(); }); @@ -250,9 +250,7 @@ describe('issues app', () => { const issueBoxFixThat = within(screen.getByRole('region', { name: 'Fix that' })); expect( - issueBoxFixThat.getByRole('button', { - name: 'issue.type.type_x_click_to_change.issue.type.CODE_SMELL', - }) + issueBoxFixThat.getByLabelText('issue.type.type_x_click_to_change.issue.type.CODE_SMELL') ).toBeInTheDocument(); await user.click( @@ -270,68 +268,12 @@ describe('issues app', () => { await user.click(screen.getByRole('button', { name: 'apply' })); expect( - issueBoxFixThat.getByRole('button', { - name: 'issue.type.type_x_click_to_change.issue.type.BUG', - }) + issueBoxFixThat.getByLabelText('issue.type.type_x_click_to_change.issue.type.BUG') ).toBeInTheDocument(); }); }); describe('filtering', () => { - it('should handle filtering from a specific issue properly', async () => { - const user = userEvent.setup(); - renderIssueApp(); - await waitOnDataLoaded(); - - // Ensure issue type filter is unchecked - expect(ui.codeSmellIssueTypeFilter.get()).not.toBeChecked(); - expect(ui.vulnerabilityIssueTypeFilter.get()).not.toBeChecked(); - expect(ui.issueItem1.get()).toBeInTheDocument(); - expect(ui.issueItem2.get()).toBeInTheDocument(); - - // Open filter similar issue dropdown for issue 2 (Code smell) - await user.click( - await within(ui.issueItem2.get()).findByRole('button', { - name: 'issue.filter_similar_issues', - }) - ); - await user.click( - await within(ui.issueItem2.get()).findByRole('button', { - name: 'issue.type.CODE_SMELL', - }) - ); - - expect(ui.codeSmellIssueTypeFilter.get()).toBeChecked(); - expect(ui.vulnerabilityIssueTypeFilter.get()).not.toBeChecked(); - expect(ui.issueItem1.query()).not.toBeInTheDocument(); - expect(ui.issueItem2.get()).toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: 'issues.facet.owaspTop10_2021' }) - ).not.toBeInTheDocument(); - - // Clear filters - await user.click(ui.clearAllFilters.get()); - - // Open filter similar issue dropdown for issue 3 (Vulnerability) - await user.click( - await within(await ui.issueItem1.find()).findByRole('button', { - name: 'issue.filter_similar_issues', - }) - ); - await user.click( - await within(await ui.issueItem1.find()).findByRole('button', { - name: 'issue.type.VULNERABILITY', - }) - ); - - expect(ui.codeSmellIssueTypeFilter.get()).not.toBeChecked(); - expect(ui.vulnerabilityIssueTypeFilter.get()).toBeChecked(); - expect(ui.issueItem1.get()).toBeInTheDocument(); - expect(ui.issueItem2.query()).not.toBeInTheDocument(); - // Standards should now be expanded and Owasp should be visible - expect(screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' })).toBeVisible(); - }); - it('should combine sidebar filters properly', async () => { const user = userEvent.setup(); renderIssueApp(); @@ -688,35 +630,27 @@ describe('issues item', () => { // Change issue type await user.click( - listItem.getByRole('button', { - name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`, - }) + listItem.getByLabelText('issue.type.type_x_click_to_change.issue.type.CODE_SMELL') ); expect(listItem.getByText('issue.type.BUG')).toBeInTheDocument(); expect(listItem.getByText('issue.type.VULNERABILITY')).toBeInTheDocument(); await user.click(listItem.getByText('issue.type.VULNERABILITY')); expect( - listItem.getByRole('button', { - name: `issue.type.type_x_click_to_change.issue.type.VULNERABILITY`, - }) + listItem.getByLabelText('issue.type.type_x_click_to_change.issue.type.VULNERABILITY') ).toBeInTheDocument(); // Change issue severity expect(listItem.getByText('severity.MAJOR')).toBeInTheDocument(); await user.click( - listItem.getByRole('button', { - name: `issue.severity.severity_x_click_to_change.severity.MAJOR`, - }) + listItem.getByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR') ); expect(listItem.getByText('severity.MINOR')).toBeInTheDocument(); expect(listItem.getByText('severity.INFO')).toBeInTheDocument(); await user.click(listItem.getByText('severity.MINOR')); expect( - listItem.getByRole('button', { - name: `issue.severity.severity_x_click_to_change.severity.MINOR`, - }) + listItem.getByLabelText('issue.severity.severity_x_click_to_change.severity.MINOR') ).toBeInTheDocument(); // Change issue status @@ -728,9 +662,7 @@ describe('issues item', () => { await user.click(listItem.getByText('issue.transition.confirm')); expect( - listItem.getByRole('button', { - name: `issue.transition.status_x_click_to_change.issue.status.CONFIRMED`, - }) + listItem.getByLabelText('issue.transition.status_x_click_to_change.issue.status.CONFIRMED') ).toBeInTheDocument(); // As won't fix @@ -745,65 +677,27 @@ describe('issues item', () => { ).not.toBeInTheDocument(); // Assign issue to a different user + await user.click( - listItem.getByRole('button', { - name: `issue.assign.unassigned_click_to_assign`, - }) + listItem.getByRole('combobox', { name: 'issue.assign.unassigned_click_to_assign' }) ); - await user.click(listItem.getByRole('searchbox', { name: 'search.search_for_users' })); - await user.keyboard('luke'); - expect(listItem.getByText('Skywalker')).toBeInTheDocument(); - await user.keyboard('{ArrowUp}{enter}'); + await user.click(screen.getByLabelText('search.search_for_users')); + + await act(async () => { + await user.keyboard('luke'); + }); + expect(screen.getByText('Skywalker')).toBeInTheDocument(); + + await user.click(screen.getByText('Skywalker')); + await listItem.findByRole('combobox', { + name: 'issue.assign.assigned_to_x_click_to_change.luke', + }); expect( - listItem.getByRole('button', { + listItem.getByRole('combobox', { name: 'issue.assign.assigned_to_x_click_to_change.luke', }) ).toBeInTheDocument(); - // Add comment to the issue - await user.click( - listItem.getByRole('button', { - name: `issue.comment.add_comment`, - }) - ); - await user.keyboard('comment'); - await user.click(listItem.getByRole('button', { name: 'issue.comment.formlink' })); - expect(listItem.getByText('comment')).toBeInTheDocument(); - - // Cancel editing the comment - await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' })); - await user.keyboard('New '); - await user.click(listItem.getByRole('button', { name: 'issue.comment.edit.cancel' })); - expect(listItem.queryByText('New comment')).not.toBeInTheDocument(); - - // Edit the comment - await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' })); - await user.keyboard('New '); - await user.click(listItem.getByText('save')); - expect(listItem.getByText('New comment')).toBeInTheDocument(); - - // Delete the comment - await user.click(listItem.getByRole('button', { name: 'issue.comment.delete' })); - await user.click(listItem.getByRole('button', { name: 'delete' })); // Confirm button - expect(listItem.queryByText('New comment')).not.toBeInTheDocument(); - - // Add comment using keyboard - await user.click( - listItem.getByRole('button', { - name: `issue.comment.add_comment`, - }) - ); - await user.keyboard('comment'); - await user.keyboard('{Control>}{enter}{/Control}'); - expect(listItem.getByText('comment')).toBeInTheDocument(); - - // Edit the comment using keyboard - await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' })); - await user.keyboard('New '); - await user.keyboard('{Control>}{enter}{/Control}'); - expect(listItem.getByText('New comment')).toBeInTheDocument(); - await user.keyboard('{Escape}'); - // Change tags expect(listItem.getByText('issue.no_tag')).toBeInTheDocument(); await user.click(listItem.getByText('issue.no_tag')); @@ -816,13 +710,13 @@ describe('issues item', () => { expect(listItem.getByTitle('accessibility, android')).toBeInTheDocument(); // Unselect - await user.click(screen.getByText('accessibility')); - expect(screen.getByTitle('android')).toBeInTheDocument(); + await user.click(screen.getByRole('checkbox', { name: 'accessibility' })); + expect(listItem.getByTitle('android')).toBeInTheDocument(); await user.click(screen.getByRole('searchbox', { name: 'search.search_for_tags' })); await user.keyboard('addNewTag'); expect( - screen.getByRole('checkbox', { name: 'create_new_element: addnewtag' }) + screen.getByRole('checkbox', { name: 'issue.create_tag: addnewtag' }) ).toBeInTheDocument(); }); @@ -843,12 +737,6 @@ describe('issues item', () => { }) ).not.toBeInTheDocument(); - await user.click( - screen.getByRole('button', { - name: `issue.comment.add_comment`, - }) - ); - expect(screen.queryByRole('button', { name: 'issue.comment.submit' })).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: `issue.transition.status_x_click_to_change.issue.status.OPEN`, @@ -867,12 +755,13 @@ describe('issues item', () => { renderIssueApp(); // Select an issue with an advanced rule - await user.click(await ui.issueItem5.find()); + await user.click(await ui.issueItemAction5.find()); // open severity popup on key press 'i' + await user.keyboard('i'); - expect(screen.getByRole('button', { name: 'severity.MINOR' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'severity.INFO' })).toBeInTheDocument(); + expect(screen.getByText('severity.MINOR')).toBeInTheDocument(); + expect(screen.getByText('severity.INFO')).toBeInTheDocument(); // open status popup on key press 'f' await user.keyboard('f'); @@ -885,16 +774,18 @@ describe('issues item', () => { await user.keyboard('{Escape}'); // open tags popup on key press 't' - await user.keyboard('t'); - expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument(); - expect(screen.getByText('android')).toBeInTheDocument(); - expect(screen.getByText('accessibility')).toBeInTheDocument(); - // closing tags popup - await user.click(screen.getByText('issue.no_tag')); - - // open assign popup on key press 'a' - await user.keyboard('a'); - expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument(); + + // needs to be fixed with the new header from ambroise! + // await user.keyboard('t'); + // expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument(); + // expect(screen.getByText('android')).toBeInTheDocument(); + // expect(screen.getByText('accessibility')).toBeInTheDocument(); + // // closing tags popup + // await user.click(screen.getByText('issue.no_tag')); + + // // open assign popup on key press 'a' + // await user.keyboard('a'); + // expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument(); }); it('should not open the actions popup using keyboard shortcut when keyboard shortcut flag is disabled', async () => { @@ -921,7 +812,7 @@ describe('issues item', () => { const user = userEvent.setup(); renderIssueApp(); - await user.click(await ui.issueItem4.find()); + await user.click(await ui.issueItemAction4.find()); expect(screen.getByRole('link', { name: 'location 1' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'location 2' })).toBeInTheDocument(); @@ -970,7 +861,7 @@ describe('issues item', () => { renderIssueApp(); // Select an issue with an advanced rule - await user.click(await ui.issueItem7.find()); + await user.click(await ui.issueItemAction7.find()); expect( screen.getByRole('heading', { diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx index 1c81af6e56b..de50f050463 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx @@ -17,9 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import styled from '@emotion/styled'; +import { Badge, themeBorder, themeColor, themeContrast } from 'design-system'; import * as React from 'react'; import BranchIcon from '../../../components/icons/BranchIcon'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { collapsePath, limitComponentName } from '../../../helpers/path'; import { ComponentQualifier, isView } from '../../../types/component'; @@ -52,15 +53,13 @@ export default function ComponentBreadcrumbs({ const projectName = [issue.projectName, issue.branch].filter((s) => !!s).join(' - '); return ( -
- - {displayProject && ( {limitComponentName(issue.projectName)} @@ -73,15 +72,30 @@ export default function ComponentBreadcrumbs({ {issue.branch} ) : ( - {translate('branches.main_branch')} + {translate('branches.main_branch')} )} )} - + )} {collapsePath(componentName || '')} -
+ ); } + +const DivStyled = styled.div` + color: ${themeContrast('subnavigation')}; + background-color: ${themeColor('subnavigation')}; + &:not(:last-child) { + border-bottom: ${themeBorder('default')}; + } +`; + +const SlashSeparator = styled.span` + &:after { + content: '/'; + color: rgba(68, 68, 68, 0.3); + } +`; diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx index b66235a08ca..1ee28169b96 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx @@ -21,11 +21,11 @@ import * as React from 'react'; import { setIssueAssignee } from '../../../api/issues'; import Link from '../../../components/common/Link'; import LinkIcon from '../../../components/icons/LinkIcon'; +import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting'; import { updateIssue } from '../../../components/issue/actions'; import IssueActionsBar from '../../../components/issue/components/IssueActionsBar'; import IssueChangelog from '../../../components/issue/components/IssueChangelog'; import IssueMessageTags from '../../../components/issue/components/IssueMessageTags'; -import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index ac4a86b744e..bca70d2b39c 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -19,7 +19,7 @@ */ import classNames from 'classnames'; -import { FlagMessage, ToggleButton } from 'design-system'; +import { ButtonSecondary, Checkbox, FlagMessage, ToggleButton } from 'design-system'; import { debounce, keyBy, omit, without } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; @@ -33,9 +33,7 @@ import { PageContext } from '../../../app/components/indexation/PageUnavailableD import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import EmptySearch from '../../../components/common/EmptySearch'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; -import Checkbox from '../../../components/controls/Checkbox'; import ListFooter from '../../../components/controls/ListFooter'; -import { Button } from '../../../components/controls/buttons'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; @@ -924,14 +922,14 @@ export class App extends React.PureComponent { title={translate('issues.select_all_issues')} /> - + {bulkChangeModal && ( { }} loading={loadingMore} total={paging.total} + useMIUIButtons={true} /> )} diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx index a49bb1d9425..6cf790b1cf1 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx @@ -65,9 +65,7 @@ export default class IssuesList extends React.PureComponent { return (
  • -
    - -
    +
    • {issues.map((issue) => ( diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx index a1fe1c5d19e..db287f5c81d 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - +import styled from '@emotion/styled'; +import { themeBorder, themeColor } from 'design-system'; import * as React from 'react'; import Issue from '../../../components/issue/Issue'; import { BranchLike } from '../../../types/branch-like'; @@ -103,7 +104,7 @@ export default class ListItem extends React.PureComponent { const { branchLike, issue } = this.props; return ( -
    • (this.nodeRef = node)}> + (this.nodeRef = node)}> { onChange={this.props.onChange} onCheck={this.props.onCheck} onClick={this.props.onClick} - onFilter={this.handleFilter} onPopupToggle={this.props.onPopupToggle} openPopup={this.props.openPopup} selected={this.props.selected} /> -
    • + ); } } + +const IssueItem = styled.li` + box-sizing: border-box; + border: ${themeBorder('default', 'transparent')}; + border-top: ${themeBorder('default')}; + outline: none; + + &.selected { + border: ${themeBorder('default', 'tableRowSelected')}; + } + + &:hover { + background: ${themeColor('tableRowHover')}; + } + + &:last-child { + border-bottom: ${themeBorder('default')}; + } +`; diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx index 57e36e28038..e37cdc3444c 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { KeyboardHint } from 'design-system'; import * as React from 'react'; import HomePageSelect from '../../../components/controls/HomePageSelect'; -import PageShortcutsTooltip from '../../../components/ui/PageShortcutsTooltip'; import { translate } from '../../../helpers/l10n'; import { Paging } from '../../../types/types'; import IssuesCounter from './IssuesCounter'; @@ -36,20 +36,14 @@ export default function PageActions(props: PageActionsProps) { const { canSetHome, effortTotal, paging, selectedIndex } = props; return ( -
      - +
      + + -
      - {paging != null && } - {effortTotal !== undefined && } -
      + {paging != null && } + {effortTotal !== undefined && } - {canSetHome && ( - - )} + {canSetHome && }
      ); } diff --git a/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx b/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx index 2ab6c808732..d94a92a978f 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx @@ -24,14 +24,12 @@ import { formatMeasure } from '../../../helpers/measures'; export default function TotalEffort({ effort }: { effort: number }) { return ( -
      -
      - {formatMeasure(effort, 'WORK_DUR')} }} - /> -
      +
      + {formatMeasure(effort, 'WORK_DUR')} }} + />
      ); } diff --git a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx index 332697a4a83..ced46d9607c 100644 --- a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx +++ b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx @@ -35,6 +35,15 @@ export const componentsHandler = new ComponentsServiceMock(); export const ui = { loading: byLabelText('loading'), + issueItemAction1: byRole('link', { name: 'Issue with no location message' }), + issueItemAction2: byRole('link', { name: 'FlowIssue' }), + issueItemAction3: byRole('link', { name: 'Issue on file' }), + issueItemAction4: byRole('link', { name: 'Fix this' }), + issueItemAction5: byRole('link', { name: 'Fix that' }), + issueItemAction6: byRole('link', { name: 'Second issue' }), + issueItemAction7: byRole('link', { name: 'Issue with tags' }), + issueItemAction8: byRole('link', { name: 'Issue on page 2' }), + issueItems: byRole('region'), issueItem1: byRole('region', { name: 'Issue with no location message' }), diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx index caccba9fa30..1f2868d2870 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx @@ -69,8 +69,6 @@ export interface Props { component: string; componentMeasures?: Measure[]; displayAllIssues?: boolean; - displayIssueLocationsCount?: boolean; - displayIssueLocationsLink?: boolean; displayLocationMarkers?: boolean; highlightedLine?: number; // `undefined` elements mean they are located in a different file, @@ -505,8 +503,6 @@ export default class SourceViewer extends React.PureComponent { { line={line} openIssuesByLine={openIssuesByLine} branchLike={this.props.branchLike} - displayIssueLocationsCount={this.props.displayIssueLocationsCount} - displayIssueLocationsLink={this.props.displayIssueLocationsLink} issuePopup={this.props.issuePopup} onIssueChange={this.props.onIssueChange} onIssueClick={this.props.onIssueSelect} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx index b3481bba73a..0ec779075fb 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx @@ -21,7 +21,7 @@ import { queryHelpers, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { act } from 'react-dom/test-utils'; -import { byRole } from 'testing-library-selector'; +import { byText } from 'testing-library-selector'; import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock'; import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; import { HttpStatus } from '../../../helpers/request'; @@ -30,6 +30,13 @@ import { renderComponent } from '../../../helpers/testReactTestingUtils'; import SourceViewer from '../SourceViewer'; import loadIssues from '../helpers/loadIssues'; +jest.mock('../../../api/components'); +jest.mock('../../../api/issues'); +// The following 2 mocks are needed, because IssuesServiceMock mocks more than it should. +// This should be removed once IssuesServiceMock is cleaned up. +jest.mock('../../../api/rules'); +jest.mock('../../../api/users'); + jest.mock('../helpers/loadIssues', () => ({ __esModule: true, default: jest.fn().mockResolvedValue([]), @@ -44,8 +51,8 @@ jest.mock('../helpers/lines', () => { }); const ui = { - codeSmellTypeButton: byRole('button', { name: 'issue.type.CODE_SMELL' }), - minorSeverityButton: byRole('button', { name: /severity.MINOR/ }), + codeSmellTypeButton: byText('issue.type.CODE_SMELL'), + minorSeverityButton: byText(/severity.MINOR/), }; const componentsHandler = new ComponentsServiceMock(); @@ -140,15 +147,13 @@ it('should be able to interact with issue action', async () => { //Open Issue type await user.click( - await screen.findByRole('button', { name: 'issue.type.type_x_click_to_change.issue.type.BUG' }) + await screen.findByLabelText('issue.type.type_x_click_to_change.issue.type.BUG') ); expect(ui.codeSmellTypeButton.get()).toBeInTheDocument(); // Open severity await user.click( - await screen.findByRole('button', { - name: 'issue.severity.severity_x_click_to_change.severity.MAJOR', - }) + await screen.findByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR') ); expect(ui.minorSeverityButton.get()).toBeInTheDocument(); @@ -158,16 +163,12 @@ it('should be able to interact with issue action', async () => { // Change the severity await user.click( - await screen.findByRole('button', { - name: 'issue.severity.severity_x_click_to_change.severity.MAJOR', - }) + await screen.findByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR') ); expect(ui.minorSeverityButton.get()).toBeInTheDocument(); await user.click(ui.minorSeverityButton.get()); expect( - screen.getByRole('button', { - name: 'issue.severity.severity_x_click_to_change.severity.MINOR', - }) + screen.getByLabelText('issue.severity.severity_x_click_to_change.severity.MINOR') ).toBeInTheDocument(); }); @@ -271,8 +272,8 @@ it('should show issue indicator', async () => { name: 'source_viewer.issues_on_line.X_issues_of_type_Y.source_viewer.issues_on_line.show.2.issue.type.BUG.plural', }) ); - const firstIssueBox = issueRow.getByRole('region', { name: 'First Issue' }); - const secondIssueBox = issueRow.getByRole('region', { name: 'Second Issue' }); + const firstIssueBox = issueRow.getByRole('link', { name: 'First Issue' }); + const secondIssueBox = issueRow.getByRole('link', { name: 'Second Issue' }); expect(firstIssueBox).toBeInTheDocument(); expect(secondIssueBox).toBeInTheDocument(); expect( @@ -383,8 +384,6 @@ function renderSourceViewer(override?: Partial) { branchLike={undefined} component={componentsHandler.getNonEmptyFileKey()} displayAllIssues - displayIssueLocationsCount - displayIssueLocationsLink={false} displayLocationMarkers onIssueChange={jest.fn()} onIssueSelect={jest.fn()} @@ -400,8 +399,6 @@ function renderSourceViewer(override?: Partial) { branchLike={undefined} component={componentsHandler.getNonEmptyFileKey()} displayAllIssues - displayIssueLocationsCount - displayIssueLocationsLink={false} displayLocationMarkers onIssueChange={jest.fn()} onIssueSelect={jest.fn()} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap index 8688bd62ed3..a306b0f3a1e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap @@ -48,8 +48,6 @@ exports[`should render correctly 1`] = ` } } displayAllIssues={false} - displayIssueLocationsCount={true} - displayIssueLocationsLink={true} displayLocationMarkers={true} duplicationsByLine={{}} hasSourcesAfter={false} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx index 4c5350c1125..a9d639f376b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx @@ -19,15 +19,13 @@ */ import * as React from 'react'; import { BranchLike } from '../../../types/branch-like'; -import { Issue as TypeIssue, LinearIssueLocation, SourceLine } from '../../../types/types'; +import { LinearIssueLocation, SourceLine, Issue as TypeIssue } from '../../../types/types'; import Issue from '../../issue/Issue'; export interface LineIssuesListProps { branchLike: BranchLike | undefined; displayAllIssues?: boolean; displayWhyIsThisAnIssue: boolean; - displayIssueLocationsCount?: boolean; - displayIssueLocationsLink?: boolean; issuesForLine: TypeIssue[]; issuePopup: { issue: string; name: string } | undefined; issueLocationsByLine: { [line: number]: LinearIssueLocation[] }; @@ -69,8 +67,6 @@ export default function LineIssuesList(props: LineIssuesListProps) { void; onCheck?: (issue: string) => void; onClick?: (issueKey: string) => void; - onFilter?: (property: string, issue: TypeIssue) => void; onPopupToggle: (issue: string, popupName: string, open?: boolean) => void; openPopup?: string; selected: boolean; @@ -118,14 +114,11 @@ export default class Issue extends React.PureComponent { checked={this.props.checked} currentPopup={this.props.openPopup} displayWhyIsThisAnIssue={this.props.displayWhyIsThisAnIssue} - displayLocationsCount={this.props.displayLocationsCount} - displayLocationsLink={this.props.displayLocationsLink} issue={this.props.issue} onAssign={this.handleAssignement} onChange={this.props.onChange} onCheck={this.props.onCheck} onClick={this.props.onClick} - onFilter={this.props.onFilter} selected={this.props.selected} togglePopup={this.togglePopup} /> diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx index 7541c4fe87a..4be902198db 100644 --- a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx +++ b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx @@ -18,14 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { act, screen, within } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { omit, pick } from 'lodash'; import * as React from 'react'; -import { byRole, byText } from 'testing-library-selector'; +import { byLabelText, byRole, byText } from 'testing-library-selector'; import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; import { KeyboardKeys } from '../../../helpers/keycodes'; -import { mockIssueComment } from '../../../helpers/mocks/issues'; import { mockIssue, mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks'; import { findTooltipWithContent, renderApp } from '../../../helpers/testReactTestingUtils'; import { @@ -36,7 +35,6 @@ import { IssueType, } from '../../../types/issues'; import { RuleStatus } from '../../../types/rules'; -import { IssueComment } from '../../../types/types'; import Issue from '../Issue'; jest.mock('../../../helpers/preferences', () => ({ @@ -50,29 +48,12 @@ beforeEach(() => { }); describe('rendering', () => { - it('should render correctly with comments', () => { - const { ui } = getPageObject(); - renderIssue({ issue: mockIssue(false, { comments: [mockIssueCommentPosted4YearsAgo()] }) }); - - const comments = within(ui.commentsList()); - expect(comments.getByText('Leïa Skywalker')).toBeInTheDocument(); - expect(comments.getByRole('listitem')).toHaveTextContent('This is a comment, bud'); - expect(comments.getByRole('listitem')).toHaveTextContent('issue.comment.posted_on4 years ago'); - }); - - it('should render correctly for locations, issue message, line, permalink, why, and effort', async () => { + it('should render correctly for issue message and effort', async () => { const { ui } = getPageObject(); const issue = mockIssue(true, { effort: '2 days', message: 'This is an issue' }); const onClick = jest.fn(); - renderIssue({ issue, displayLocationsCount: true, displayWhyIsThisAnIssue: true, onClick }); + renderIssue({ issue, onClick }); - expect(ui.locationsBadge(7).get()).toBeInTheDocument(); - expect(ui.lineInfo(26).get()).toBeInTheDocument(); - expect(ui.permalink.get()).toHaveAttribute( - 'href', - `/project/issues?issues=${issue.key}&open=${issue.key}&id=${issue.project}` - ); - expect(ui.whyLink.get()).toBeInTheDocument(); expect(ui.effort('2 days').get()).toBeInTheDocument(); await ui.clickIssueMessage(); expect(onClick).toHaveBeenCalledWith(issue.key); @@ -89,7 +70,7 @@ describe('rendering', () => { it('should render correctly for external rule engines', () => { renderIssue({ issue: mockIssue(true, { externalRuleEngine: 'ESLINT' }) }); - expect(screen.getByText('ESLINT')).toBeInTheDocument(); + expect(screen.getByRole('status', { name: 'ESLINT' })).toBeInTheDocument(); }); it('should render the SonarLint icon correctly', () => { @@ -108,17 +89,6 @@ describe('rendering', () => { expect(onCheck).toHaveBeenCalledWith(issue.key); }); - it('should correctly render the changelog', async () => { - const { ui } = getPageObject(); - renderIssue(); - - await ui.showChangelog(); - expect( - ui.changelogRow('status', IssueStatus.Confirmed, IssueStatus.Reopened).get() - ).toBeInTheDocument(); - expect(ui.changelogRow('assign', 'luke.skywalker', 'darth.vader').get()).toBeInTheDocument(); - }); - it('should correctly render any code variants', () => { const { ui } = getPageObject(); renderIssue({ issue: mockIssue(false, { codeVariants: ['variant 1', 'variant 2'] }) }); @@ -184,41 +154,21 @@ describe('updating', () => { expect(ui.updateAssigneeBtn('luke').get()).toBeInTheDocument(); }); - it('should allow commenting', async () => { - const { ui } = getPageObject(); - const issue = mockRawIssue(false, { - actions: [IssueActions.Comment], - }); - issuesHandler.setIssueList([{ issue, snippets: {} }]); - renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key') }) }); - - // Create - await ui.addComment('Original content'); - const comments = within(ui.commentsList()); - expect(comments.getByRole('listitem')).toHaveTextContent('Original content'); - - // Update - await ui.updateComment('New content'); - expect(comments.getByRole('listitem')).toHaveTextContent('New content'); - - // Delete - await ui.deleteComment(); - expect(comments.getByRole('listitem')).toHaveTextContent('New content'); - }); - - it('should allow updating the tags', async () => { - const { ui } = getPageObject(); - const issue = mockRawIssue(false, { - tags: [], - actions: [IssueActions.SetTags], - }); - issuesHandler.setIssueList([{ issue, snippets: {} }]); - renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'tags') }) }); - - await ui.addTag('accessibility'); - await ui.addTag('android', ['accessibility']); - expect(ui.updateTagsBtn(['accessibility', 'android']).get()).toBeInTheDocument(); - }); + // Should be re-enabled when tags are re-enabled with ambroise code + // eslint-disable-next-line jest/no-commented-out-tests + // it('should allow updating the tags', async () => { + // const { ui } = getPageObject(); + // const issue = mockRawIssue(false, { + // tags: [], + // actions: [IssueActions.SetTags], + // }); + // issuesHandler.setIssueList([{ issue, snippets: {} }]); + // renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'tags') }) }); + + // await ui.addTag('accessibility'); + // await ui.addTag('android', ['accessibility']); + // expect(ui.updateTagsBtn(['accessibility', 'android']).get()).toBeInTheDocument(); + // }); }); it('should correctly handle keyboard shortcuts', async () => { @@ -265,68 +215,6 @@ it('should correctly handle keyboard shortcuts', async () => { expect(ui.updateAssigneeBtn('leia').get()).toBeInTheDocument(); }); -it('should correctly handle similar issues filtering', async () => { - const { ui, user } = getPageObject(); - const onFilter = jest.fn(); - const issue = mockIssue(false, { - ruleName: 'Rule Foo', - tags: ['accessibility', 'owasp'], - projectName: 'Project Bar', - componentLongName: 'main.js', - }); - renderIssue({ onFilter, issue }); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueTypeLink.get()); - expect(onFilter).toHaveBeenLastCalledWith('type', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueSeverityLink.get()); - expect(onFilter).toHaveBeenLastCalledWith('severity', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueStatusLink.get()); - expect(onFilter).toHaveBeenLastCalledWith('status', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueResolutionLink.get()); - expect(onFilter).toHaveBeenLastCalledWith('resolution', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueAssigneeLink.get()); - expect(onFilter).toHaveBeenLastCalledWith('assignee', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueRuleLink.get()); - expect(onFilter).toHaveBeenLastCalledWith('rule', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueTagLink('accessibility').get()); - expect(onFilter).toHaveBeenLastCalledWith('tag###accessibility', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueTagLink('owasp').get()); - expect(onFilter).toHaveBeenLastCalledWith('tag###owasp', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueProjectLink.get()); - expect(onFilter).toHaveBeenLastCalledWith('project', issue); - - await ui.showSimilarIssues(); - await user.click(ui.similarIssueFileLink.get()); - expect(onFilter).toHaveBeenLastCalledWith('file', issue); -}); - -function mockIssueCommentPosted4YearsAgo(overrides: Partial = {}) { - const date = new Date(); - date.setFullYear(date.getFullYear() - 4); - return mockIssueComment({ - authorName: 'Leïa Skywalker', - createdAt: date.toISOString(), - ...overrides, - }); -} - function getPageObject() { const user = userEvent.setup(); @@ -339,7 +227,7 @@ function getPageObject() { effort: (effort: string) => byText(`issue.x_effort.${effort}`), whyLink: byRole('link', { name: 'issue.why_this_issue.long' }), checkbox: byRole('checkbox'), - issueMessageBtn: byRole('button', { name: 'This is an issue' }), + issueMessageBtn: byRole('link', { name: 'This is an issue' }), variants: (n: number) => byText(`issue.x_code_variants.${n}`), // Changelog @@ -385,38 +273,31 @@ function getPageObject() { // Type updateTypeBtn: (currentType: IssueType) => - byRole('button', { name: `issue.type.type_x_click_to_change.issue.type.${currentType}` }), - setTypeBtn: (type: IssueType) => byRole('button', { name: `issue.type.${type}` }), + byLabelText(`issue.type.type_x_click_to_change.issue.type.${currentType}`), + setTypeBtn: (type: IssueType) => byText(`issue.type.${type}`), // Severity updateSeverityBtn: (currentSeverity: IssueSeverity) => - byRole('button', { - name: `issue.severity.severity_x_click_to_change.severity.${currentSeverity}`, - }), - setSeverityBtn: (severity: IssueSeverity) => byRole('button', { name: `severity.${severity}` }), + byLabelText(`issue.severity.severity_x_click_to_change.severity.${currentSeverity}`), + setSeverityBtn: (severity: IssueSeverity) => byText(`severity.${severity}`), // Status updateStatusBtn: (currentStatus: IssueStatus) => - byRole('button', { - name: `issue.transition.status_x_click_to_change.issue.status.${currentStatus}`, - }), - setStatusBtn: (transition: IssueTransition) => - byRole('button', { name: `issue.transition.${transition}` }), + byLabelText(`issue.transition.status_x_click_to_change.issue.status.${currentStatus}`), + setStatusBtn: (transition: IssueTransition) => byText(`issue.transition.${transition}`), // Assignee - assigneeSearchInput: byRole('searchbox'), + assigneeSearchInput: byLabelText('search.search_for_users'), updateAssigneeBtn: (currentAssignee: string) => - byRole('button', { + byRole('combobox', { name: `issue.assign.assigned_to_x_click_to_change.${currentAssignee}`, }), - setAssigneeBtn: (name: RegExp) => byRole('button', { name }), + setAssigneeBtn: (name: RegExp) => byLabelText(name), // Tags tagsSearchInput: byRole('searchbox'), updateTagsBtn: (currentTags?: string[]) => - byRole('button', { - name: `tags_list_x.${currentTags ? currentTags.join(', ') : 'issue.no_tag'}`, - }), + byText(`tags_list_x.${currentTags ? currentTags.join(', ') : 'issue.no_tag'}`), toggleTagCheckbox: (name: string) => byRole('checkbox', { name }), }; @@ -462,7 +343,9 @@ function getPageObject() { }, async updateAssignee(currentAssignee: string, newAssignee: string) { await user.click(selectors.updateAssigneeBtn(currentAssignee).get()); - await user.type(selectors.assigneeSearchInput.get(), newAssignee); + await act(async () => { + await user.type(selectors.assigneeSearchInput.get(), newAssignee); + }); await act(async () => { await user.click(selectors.setAssigneeBtn(new RegExp(newAssignee)).get()); }); @@ -479,9 +362,7 @@ function getPageObject() { async showChangelog() { await user.click(selectors.toggleChangelogBtn.get()); }, - async showSimilarIssues() { - await user.click(selectors.toggleSimilarIssuesBtn.get()); - }, + async toggleCheckbox() { await user.click(selectors.checkbox.get()); }, diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx index 3a8980252a6..28005887c65 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx @@ -17,22 +17,27 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import styled from '@emotion/styled'; import classNames from 'classnames'; +import { Badge, CommentIcon, SeparatorCircleIcon, themeColor } from 'design-system'; import * as React from 'react'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { isDefined } from '../../../helpers/types'; import { IssueActions, IssueResolution, IssueResponse, IssueType as IssueTypeEnum, } from '../../../types/issues'; +import { RuleStatus } from '../../../types/rules'; import { Issue, RawQuery } from '../../../types/types'; import Tooltip from '../../controls/Tooltip'; +import DateFromNow from '../../intl/DateFromNow'; import { updateIssue } from '../actions'; import IssueAssign from './IssueAssign'; import IssueCommentAction from './IssueCommentAction'; +import IssueMessageTags from './IssueMessageTags'; import IssueSeverity from './IssueSeverity'; -import IssueTags from './IssueTags'; import IssueTransition from './IssueTransition'; import IssueType from './IssueType'; @@ -43,7 +48,9 @@ interface Props { onChange: (issue: Issue) => void; togglePopup: (popup: string, show?: boolean) => void; className?: string; + showComments?: boolean; showCommentsInPopup?: boolean; + showLine?: boolean; } interface State { @@ -95,97 +102,141 @@ export default class IssueActionsBar extends React.PureComponent { }; render() { - const { issue, className, showCommentsInPopup } = this.props; + const { issue, className, showComments, showCommentsInPopup, showLine } = this.props; const canAssign = issue.actions.includes(IssueActions.Assign); const canComment = issue.actions.includes(IssueActions.Comment); const canSetSeverity = issue.actions.includes(IssueActions.SetSeverity); const canSetType = issue.actions.includes(IssueActions.SetType); - const canSetTags = issue.actions.includes(IssueActions.SetTags); const hasTransitions = issue.transitions.length > 0; + const hasComments = issue.comments && issue.comments.length > 0; + const IssueMetaLiClass = classNames( + className, + 'sw-body-sm sw-overflow-hidden sw-whitespace-nowrap sw-max-w-abs-150' + ); return ( -
      -
      -
      +
      +
        +
      • -
      -
      + +
    • -
    • -
      + +
    • -
    • -
      + +
    • -
    • - {issue.effort && ( -
      - - {translateWithParameters('issue.x_effort', issue.effort)} - -
      - )} - {(canComment || showCommentsInPopup) && ( - +
    + {(canComment || showCommentsInPopup) && ( + + )} + +
      +
    • + +
    • + + {issue.externalRuleEngine && ( +
    • + + {issue.externalRuleEngine} + +
    • )} -
    -
    + {issue.codeVariants && issue.codeVariants.length > 0 && ( -
    + - + <> {issue.codeVariants.length > 1 ? translateWithParameters('issue.x_code_variants', issue.codeVariants.length) : translate('issue.1_code_variant')} - + -
    + + )} -
    - -
    -
    + + {showComments && hasComments && ( + <> + + + {issue.comments?.length} + + + + )} + {showLine && isDefined(issue.textRange) && ( + <> + + + {translateWithParameters('issue.ncloc_x.short', issue.textRange.endLine)} + + + + + )} + {issue.effort && ( + <> + + {translateWithParameters('issue.x_effort', issue.effort)} + + + + )} + + + + ); } } + +const IssueMetaLi = styled.li` + color: ${themeColor('pageContentLight')}; +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.tsx index 4b739af1156..f0d7e17b4a7 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.tsx @@ -17,95 +17,140 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { LabelValueSelectOption, SearchSelectDropdown } from 'design-system'; import * as React from 'react'; -import Toggler from '../../../components/controls/Toggler'; -import { ButtonLink } from '../../../components/controls/buttons'; -import DropdownIcon from '../../../components/icons/DropdownIcon'; +import { Options, SingleValue } from 'react-select'; +import { searchUsers } from '../../../api/users'; +import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Issue } from '../../../types/types'; -import LegacyAvatar from '../../ui/LegacyAvatar'; -import SetAssigneePopup from '../popups/SetAssigneePopup'; +import { isLoggedIn, isUserActive } from '../../../types/users'; +import Avatar from '../../ui/Avatar'; interface Props { - isOpen: boolean; issue: Issue; + isOpen: boolean; canAssign: boolean; onAssign: (login: string) => void; togglePopup: (popup: string, show?: boolean) => void; } -export default class IssueAssign extends React.PureComponent { - toggleAssign = (open?: boolean) => { - this.props.togglePopup('assign', open); +const minSearchLength = 2; + +const UNASSIGNED = { value: '', label: translate('unassigned') }; + +const renderAvatar = (name?: string, avatar?: string) => ( + +); + +export default function IssueAssignee(props: Props) { + const { + canAssign, + issue: { assignee, assigneeName, assigneeLogin, assigneeAvatar }, + } = props; + + const assinedUser = assigneeName || assignee; + const { currentUser } = React.useContext(CurrentUserContext); + + const allowCurrentUserSelection = isLoggedIn(currentUser) && currentUser?.login !== assigneeLogin; + + const defaultOptions = allowCurrentUserSelection + ? [ + UNASSIGNED, + { + value: currentUser.login, + label: currentUser.name, + Icon: renderAvatar(currentUser.name, currentUser.avatar), + }, + ] + : [UNASSIGNED]; + + const controlLabel = assinedUser ? ( + <> + {renderAvatar(assinedUser, assigneeAvatar)} {assinedUser} + + ) : ( + UNASSIGNED.label + ); + + const toggleAssign = (open?: boolean) => { + props.togglePopup('assign', open); }; - handleClose = () => { - this.toggleAssign(false); + const handleClose = () => { + toggleAssign(false); }; - renderAssignee() { - const { issue } = this.props; - const assigneeName = issue.assigneeName || issue.assignee; + const handleSearchAssignees = ( + query: string, + cb: (options: Options>) => void + ) => { + searchUsers({ q: query }) + .then((result) => { + const options: Array> = result.users + .filter(isUserActive) + .map((u) => ({ + label: u.name ?? u.login, + value: u.login, + Icon: renderAvatar(u.name, u.avatar), + })); + cb(options); + }) + .catch(() => { + cb([]); + }); + }; + + const renderAssignee = () => { + const { issue } = props; + const assigneeName = (issue.assigneeActive && issue.assigneeName) || issue.assignee; if (assigneeName) { - const assigneeDisplay = - issue.assigneeActive === false - ? translateWithParameters('user.x_deleted', assigneeName) - : assigneeName; return ( - <> - - - - - {assigneeDisplay} + + + + {issue.assigneeActive + ? assigneeName + : translateWithParameters('user.x_deleted', assigneeName)} - + ); } - return {translate('unassigned')}; - } - - render() { - const { canAssign, isOpen, issue } = this.props; - const assigneeName = issue.assigneeName || issue.assignee; + return {translate('unassigned')}; + }; - if (canAssign) { - return ( -
    - } - > - - {this.renderAssignee()} - - - -
    - ); + const handleAssign = (userOption: SingleValue>) => { + if (userOption) { + props.onAssign(userOption.value); } + }; - return this.renderAssignee(); + if (!canAssign) { + return renderAssignee(); } + + return ( + toggleAssign(true)} + onMenuClose={handleClose} + isDiscreet + controlLabel={controlLabel} + tooShortText={translateWithParameters('search.tooShort', String(minSearchLength))} + placeholder={translate('search.search_for_users')} + aria-label={translate('search.search_for_users')} + /> + ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx index a07e5d31f16..d3e4627b93e 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx @@ -19,9 +19,7 @@ */ import * as React from 'react'; import { addIssueComment, deleteIssueComment, editIssueComment } from '../../../api/issues'; -import { ButtonLink } from '../../../components/controls/buttons'; import Toggler from '../../../components/controls/Toggler'; -import { translate } from '../../../helpers/l10n'; import { Issue, IssueComment } from '../../../types/types'; import { updateIssue } from '../actions'; import CommentListPopup from '../popups/CommentListPopup'; @@ -93,28 +91,7 @@ export default class IssueCommentAction extends React.PureComponent { /> ) } - > - - - {showCommentsInPopup && comments && ( - - {comments.length}{' '} - {translate( - comments.length === 1 - ? 'issue.comment.formlink.total' - : 'issue.comment.formlink.total.plural' - )} - - )} - {!showCommentsInPopup && translate('issue.comment.formlink')} - - - + /> ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx index 521b390d324..458b2f9363c 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx @@ -17,17 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { StandoutLink } from 'design-system'; import * as React from 'react'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { getComponentIssuesUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; -import { RuleStatus } from '../../../types/rules'; import { Issue } from '../../../types/types'; import Link from '../../common/Link'; -import { ButtonPlain } from '../../controls/buttons'; import { IssueMessageHighlighting } from '../IssueMessageHighlighting'; -import IssueMessageTags from './IssueMessageTags'; export interface IssueMessageProps { onClick?: () => void; @@ -39,7 +37,7 @@ export interface IssueMessageProps { export default function IssueMessage(props: IssueMessageProps) { const { issue, branchLike, displayWhyIsThisAnIssue } = props; - const { externalRuleEngine, quickFixAvailable, message, messageFormattings, ruleStatus } = issue; + const { message, messageFormattings } = issue; const whyIsThisAnIssueUrl = getComponentIssuesUrl(issue.project, { ...getBranchLikeQuery(branchLike), @@ -51,22 +49,16 @@ export default function IssueMessage(props: IssueMessageProps) { return ( <> -
    - {props.onClick ? ( - - - - ) : ( - - - - )} - -
    + {props.onClick ? ( + + + + ) : ( + + + + )} + {displayWhyIsThisAnIssue && ( ; + togglePopup: (popup: string, show?: boolean) => void; setIssueProperty: ( property: keyof Issue, popup: string, apiCall: (query: RawQuery) => Promise, value: string ) => void; - togglePopup: (popup: string, show?: boolean) => void; } export default class IssueSeverity extends React.PureComponent { - toggleSetSeverity = (open?: boolean) => { - this.props.togglePopup('set-severity', open); + setSeverity = ({ value }: { value: string }) => { + this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, value); + this.toggleSetSeverity(false); }; - setSeverity = (severity: string) => { - this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, severity); + toggleSetSeverity = (open?: boolean) => { + this.props.togglePopup('set-severity', open); }; handleClose = () => { @@ -56,31 +54,40 @@ export default class IssueSeverity extends React.PureComponent { render() { const { issue } = this.props; + const SEVERITY = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; + const typesOptions = SEVERITY.map((severity) => ({ + label: translate('severity', severity), + value: severity, + Icon: , + })); + if (this.props.canSetSeverity) { return ( -
    - } - > - - - - - -
    + this.toggleSetSeverity(true)} + setValue={this.setSeverity} + value={issue.severity} + /> ); } - return ; + return ( + + + {translate('severity', issue.severity)} + + ); } } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx index 3f4a14d1a37..7ba43208c0f 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.tsx @@ -17,22 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { PopupPlacement, Tags } from 'design-system'; import * as React from 'react'; import { setIssueTags } from '../../../api/issues'; -import { ButtonLink } from '../../../components/controls/buttons'; -import Toggler from '../../../components/controls/Toggler'; import { translate } from '../../../helpers/l10n'; import { Issue } from '../../../types/types'; -import TagsList from '../../tags/TagsList'; +import Tooltip from '../../controls/Tooltip'; import { updateIssue } from '../actions'; -import SetIssueTagsPopup from '../popups/SetIssueTagsPopup'; +import IssueTagsPopup from '../popups/IssueTagsPopup'; interface Props { canSetTags: boolean; - isOpen: boolean; issue: Pick; onChange: (issue: Issue) => void; togglePopup: (popup: string, show?: boolean) => void; + open?: boolean; } export default class IssueTags extends React.PureComponent { @@ -56,39 +55,23 @@ export default class IssueTags extends React.PureComponent { }; render() { - const { issue } = this.props; + const { issue, open } = this.props; const { tags = [] } = issue; - if (this.props.canSetTags) { - return ( -
    - } - > - - 0 ? issue.tags : [translate('issue.no_tag')] - } - /> - - -
    - ); - } - return ( - 0 ? issue.tags : [translate('issue.no_tag')]} + ariaTagsListLabel={translate('issue.tags')} + className="js-issue-edit-tags" + emptyText={translate('issue.no_tag')} + menuId="issue-tags-menu" + overlay={} + popupPlacement={PopupPlacement.Bottom} + tags={tags} + tagsToDisplay={2} + tooltip={Tooltip} + open={open} + onClose={this.handleClose} /> ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx index 4df5ca5a8e7..36e52814a81 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx @@ -18,120 +18,45 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import Link from '../../../components/common/Link'; -import Tooltip from '../../../components/controls/Tooltip'; -import LinkIcon from '../../../components/icons/LinkIcon'; -import { getBranchLikeQuery } from '../../../helpers/branch-like'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { formatMeasure } from '../../../helpers/measures'; -import { getComponentIssuesUrl } from '../../../helpers/urls'; + import { BranchLike } from '../../../types/branch-like'; -import { IssueType } from '../../../types/issues'; -import { MetricType } from '../../../types/metrics'; +import { IssueActions } from '../../../types/issues'; import { Issue } from '../../../types/types'; -import LocationIndex from '../../common/LocationIndex'; -import IssueChangelog from './IssueChangelog'; import IssueMessage from './IssueMessage'; -import SimilarIssuesFilter from './SimilarIssuesFilter'; +import IssueTags from './IssueTags'; export interface IssueTitleBarProps { + currentPopup?: string; branchLike?: BranchLike; onClick?: () => void; - currentPopup?: string; displayWhyIsThisAnIssue?: boolean; - displayLocationsCount?: boolean; - displayLocationsLink?: boolean; issue: Issue; - onFilter?: (property: string, issue: Issue) => void; + onChange: (issue: Issue) => void; togglePopup: (popup: string, show?: boolean) => void; } export default function IssueTitleBar(props: IssueTitleBarProps) { - const { issue, displayWhyIsThisAnIssue } = props; - const hasSimilarIssuesFilter = props.onFilter != null; - - const locationsCount = - issue.secondaryLocations.length + - issue.flows.reduce((sum, locations) => sum + locations.length, 0) + - issue.flowsWithType.reduce((sum, { locations }) => sum + locations.length, 0); - - const locationsBadge = ( - - {locationsCount} - - ); - - const displayLocations = props.displayLocationsCount && locationsCount > 0; - - const issueUrl = getComponentIssuesUrl(issue.project, { - ...getBranchLikeQuery(props.branchLike), - issues: issue.key, - open: issue.key, - types: issue.type === IssueType.SecurityHotspot ? issue.type : undefined, - }); + const { issue, displayWhyIsThisAnIssue, currentPopup } = props; + const canSetTags = issue.actions.includes(IssueActions.SetTags); return ( -
    - -
    -
    -
    - -
    - {issue.textRange != null && ( -
    - - L{issue.textRange.endLine} - -
    - )} - {displayLocations && ( -
    - {props.displayLocationsLink ? ( - - {locationsBadge} - - ) : ( - locationsBadge - )} -
    - )} -
    - - - -
    - {hasSimilarIssuesFilter && ( -
    - -
    - )} -
    +
    +
    + +
    +
    +
    ); diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx index 7e604e9ffe9..233656ae3d0 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx @@ -17,16 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DiscreetSelect } from 'design-system'; import * as React from 'react'; +import { GroupBase, OptionProps, components } from 'react-select'; import { setIssueTransition } from '../../../api/issues'; -import { ButtonLink } from '../../../components/controls/buttons'; -import Toggler from '../../../components/controls/Toggler'; -import DropdownIcon from '../../../components/icons/DropdownIcon'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Issue } from '../../../types/types'; +import { LabelValueSelectOption } from '../../controls/Select'; +import StatusIcon from '../../icons/StatusIcon'; import StatusHelper from '../../shared/StatusHelper'; import { updateIssue } from '../actions'; -import SetTransitionPopup from '../popups/SetTransitionPopup'; interface Props { hasTransitions: boolean; @@ -37,10 +37,10 @@ interface Props { } export default class IssueTransition extends React.PureComponent { - setTransition = (transition: string) => { + setTransition = ({ value }: { value: string }) => { updateIssue( this.props.onChange, - setIssueTransition({ issue: this.props.issue.key, transition }) + setIssueTransition({ issue: this.props.issue.key, transition: value }) ); this.toggleSetTransition(false); }; @@ -56,43 +56,59 @@ export default class IssueTransition extends React.PureComponent { render() { const { issue } = this.props; + const transitions = issue.transitions.map((transition) => ({ + label: translate('issue.transition', transition), + value: transition, + Icon: , + })); + if (this.props.hasTransitions) { return ( -
    - - } - > - - - - - -
    + , + IsMulti extends boolean = false, + Group extends GroupBase