]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9435 Manage issues' popup at the list level
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 17 Aug 2017 07:10:21 +0000 (09:10 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 17 Aug 2017 14:42:34 +0000 (16:42 +0200)
19 files changed:
server/sonar-web/src/main/js/apps/issues/components/App.js
server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
server/sonar-web/src/main/js/apps/issues/components/ListItem.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
server/sonar-web/src/main/js/components/issue/Issue.js
server/sonar-web/src/main/js/components/issue/IssueView.js
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js

index 56ef5e68d133d9207802ac413a66836dad8d28c1..199c814ce926c0a3a56cbd05027d1ec225a5903d 100644 (file)
@@ -86,6 +86,10 @@ export type State = {
   myIssues: boolean,
   openFacets: { [string]: boolean },
   openIssue: ?Issue,
+  openPopup: ?{
+    issue: string,
+    name: string
+  },
   paging?: Paging,
   query: Query,
   referencedComponents: { [string]: ReferencedComponent },
@@ -117,6 +121,7 @@ export default class App extends React.PureComponent {
       myIssues: areMyIssuesSelected(props.location.query),
       openFacets: { resolutions: true, types: true },
       openIssue: null,
+      openPopup: null,
       query: parseQuery(props.location.query),
       referencedComponents: {},
       referencedLanguages: {},
@@ -594,6 +599,19 @@ export default class App extends React.PureComponent {
     });
   };
 
+  handlePopupToggle = (issue /*: string */, popupName /*: string */, open /*: ?boolean */) => {
+    this.setState((state /*: State */) => {
+      const samePopup =
+        state.openPopup && state.openPopup.name === popupName && state.openPopup.issue === issue;
+      if (open !== false && !samePopup) {
+        return { openPopup: { issue, name: popupName } };
+      } else if (open !== true && samePopup) {
+        return { openPopup: null };
+      }
+      return state;
+    });
+  };
+
   handleIssueCheck = (issue /*: string */) => {
     this.setState(state => ({
       checked: state.checked.includes(issue)
@@ -792,6 +810,8 @@ export default class App extends React.PureComponent {
             onIssueChange={this.handleIssueChange}
             onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined}
             onIssueClick={this.openIssue}
+            onPopupToggle={this.handlePopupToggle}
+            openPopup={this.state.openPopup}
             organization={organization}
             selectedIssue={selectedIssue}
           />}
index 88ba739aa8b4a3be94ac05df1e011248faed364a..8a405e94c33f242a6596fc36fd21834d11a4bd8c 100644 (file)
@@ -32,6 +32,8 @@ type Props = {|
   onIssueChange: Issue => void,
   onIssueCheck?: string => void,
   onIssueClick: string => void,
+  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
+  openPopup: ?{ issue: string, name: string},
   organization?: { key: string },
   selectedIssue: ?Issue
 |};
@@ -41,7 +43,7 @@ export default class IssuesList extends React.PureComponent {
   /*:: props: Props; */
 
   render() {
-    const { checked, component, issues, selectedIssue } = this.props;
+    const { checked, component, issues, openPopup, selectedIssue } = this.props;
 
     return (
       <div>
@@ -55,6 +57,8 @@ export default class IssuesList extends React.PureComponent {
             onCheck={this.props.onIssueCheck}
             onClick={this.props.onIssueClick}
             onFilterChange={this.props.onFilterChange}
+            onPopupToggle={this.props.onPopupToggle}
+            openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : null}
             organization={this.props.organization}
             previousIssue={index > 0 ? issues[index - 1] : null}
             selected={selectedIssue != null && selectedIssue.key === issue.key}
index 7189ff6f84a898c096e8099278c3955ca34af8a7..20bd23dfb9f9f096a36ce3dce9bcbd589c10b0d2 100644 (file)
@@ -33,6 +33,8 @@ type Props = {|
   onCheck?: string => void,
   onClick: string => void,
   onFilterChange: (changes: {}) => void,
+  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
+  openPopup: ?string,
   organization?: { key: string },
   previousIssue: ?Object,
   selected: boolean
@@ -107,6 +109,8 @@ export default class ListItem extends React.PureComponent {
           onCheck={this.props.onCheck}
           onClick={this.props.onClick}
           onFilter={this.handleFilter}
+          onPopupToggle={this.props.onPopupToggle}
+          openPopup={this.props.openPopup}
           selected={this.props.selected}
         />
       </div>
index 217e33ce936abc01ff01d23a68cb5289bd3d21a5..d656865b0ec616d62dc66a1110d5d67b0c3ccc4a 100644 (file)
@@ -99,6 +99,10 @@ type State = {
   notAccessible: boolean,
   notExist: boolean,
   openIssuesByLine: { [number]: boolean },
+  openPopup: ?{
+    issue: string,
+    name: string
+  },
   selectedIssue?: string,
   sources?: Array<SourceLine>,
   sourceRemoved: boolean,
@@ -151,6 +155,7 @@ export default class SourceViewerBase extends React.PureComponent {
       notAccessible: false,
       notExist: false,
       openIssuesByLine: {},
+      openPopup: null,
       selectedIssue: props.selectedIssue,
       selectedIssueLocation: null,
       sourceRemoved: false,
@@ -470,6 +475,19 @@ export default class SourceViewerBase extends React.PureComponent {
     }
   };
 
+  handlePopupToggle = (issue /*: string */, popupName /*: string */, open /*: ?boolean */) => {
+    this.setState((state /*: State */) => {
+      const samePopup =
+        state.openPopup && state.openPopup.name === popupName && state.openPopup.issue === issue;
+      if (open !== false && !samePopup) {
+        return { openPopup: { issue, name: popupName } };
+      } else if (open !== true && samePopup) {
+        return { openPopup: null };
+      }
+      return state;
+    });
+  };
+
   displayLinePopup(line /*: number */, element /*: HTMLElement */) {
     const popup = new LineActionsPopupView({
       line,
@@ -571,6 +589,8 @@ export default class SourceViewerBase extends React.PureComponent {
         onIssuesClose={this.handleCloseIssues}
         onLineClick={this.handleLineClick}
         onLocationSelect={this.props.onLocationSelect}
+        onPopupToggle={this.handlePopupToggle}
+        openPopup={this.state.openPopup}
         onSCMClick={this.handleSCMClick}
         onSymbolClick={this.handleSymbolClick}
         openIssuesByLine={this.state.openIssuesByLine}
index b739c6b08401ac16a5a10a510499f0a4a6f3e82a..f928aa8f5fc30174133d32afe45d70bc4d73164f 100644 (file)
@@ -68,6 +68,8 @@ export default class SourceViewerCode extends React.PureComponent {
     onSCMClick: (SourceLine, HTMLElement) => void,
     onSymbolClick: (Array<string>) => void,
     openIssuesByLine: { [number]: boolean },
+    onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
+    openPopup: ?{ issue: string, name: string},
     scroll?: HTMLElement => void,
     selectedIssue: string | null,
     sources: Array<SourceLine>,
@@ -174,6 +176,8 @@ export default class SourceViewerCode extends React.PureComponent {
         onSCMClick={this.props.onSCMClick}
         onSymbolClick={this.props.onSymbolClick}
         openIssues={this.props.openIssuesByLine[line.line] || false}
+        onPopupToggle={this.props.onPopupToggle}
+        openPopup={this.props.openPopup}
         previousLine={index > 0 ? sources[index - 1] : undefined}
         scroll={this.props.scroll}
         secondaryIssueLocations={optimizedSecondaryIssueLocations}
index 0e66fbbe50a12f6631cf56a36960d9b5cd5d70f7..10f84254b27514cca7f9d48449e274ceac379c4d 100644 (file)
@@ -62,6 +62,8 @@ type Props = {|
   onSCMClick: (SourceLine, HTMLElement) => void,
   onSymbolClick: (Array<string>) => void,
   openIssues: boolean,
+  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
+  openPopup: ?{ issue: string, name: string},
   previousLine?: SourceLine,
   scroll?: HTMLElement => void,
   secondaryIssueLocations: Array<{
@@ -150,6 +152,8 @@ export default class Line extends React.PureComponent {
           onIssueSelect={this.props.onIssueSelect}
           onLocationSelect={this.props.onLocationSelect}
           onSymbolClick={this.props.onSymbolClick}
+          onPopupToggle={this.props.onPopupToggle}
+          openPopup={this.props.openPopup}
           scroll={this.props.scroll}
           secondaryIssueLocations={this.props.secondaryIssueLocations}
           selectedIssue={this.props.selectedIssue}
index e9493b8a8b9efb71a52912babbff0f34f9921951..e2bd3eaeb7eaa1364b73b4a580564de62d10cd6a 100644 (file)
@@ -40,6 +40,8 @@ type Props = {|
   onIssueSelect: (issueKey: string) => void,
   onLocationSelect?: number => void,
   onSymbolClick: (Array<string>) => void,
+  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
+  openPopup: ?{ issue: string, name: string},
   scroll?: HTMLElement => void,
   secondaryIssueLocations: Array<{
     from: number,
@@ -221,6 +223,8 @@ export default class LineCode extends React.PureComponent {
             issues={issues}
             onIssueChange={this.props.onIssueChange}
             onIssueClick={onIssueSelect}
+            onPopupToggle={this.props.onPopupToggle}
+            openPopup={this.props.openPopup}
             selectedIssue={selectedIssue}
           />}
       </td>
index 7a498665fae33db90e4d1f1fb6afb7f32b184fbd..470cf536cea3f30bebae915e21699bcb86ac3068 100644 (file)
@@ -27,6 +27,8 @@ type Props = {
   issues: Array<IssueType>,
   onIssueChange: IssueType => void,
   onIssueClick: (issueKey: string) => void,
+  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
+  openPopup: ?{ issue: string, name: string},
   selectedIssue: string | null
 };
 */
@@ -35,7 +37,7 @@ export default class LineIssuesList extends React.PureComponent {
   /*:: props: Props; */
 
   render() {
-    const { issues, onIssueClick, selectedIssue } = this.props;
+    const { issues, onIssueClick, openPopup, selectedIssue } = this.props;
 
     return (
       <div className="issue-list">
@@ -45,6 +47,8 @@ export default class LineIssuesList extends React.PureComponent {
             key={issue.key}
             onChange={this.props.onIssueChange}
             onClick={onIssueClick}
+            onPopupToggle={this.props.onPopupToggle}
+            openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : null}
             selected={selectedIssue === issue.key}
           />
         )}
index 8a157714eeccde688135197f731371046d075674..3366bc8c8684b52a3ed1a01a5e8fdb33679afd01 100644 (file)
@@ -36,6 +36,8 @@ it('render code', () => {
       onIssueSelect={jest.fn()}
       onSelectLocation={jest.fn()}
       onSymbolClick={jest.fn()}
+      onPopupToggle={jest.fn()}
+      openPopup={null}
       selectedIssue="issue-1"
       showIssues={true}
     />
index a9a0e0763b91f408ed5ec603cc92e8ca2f96bef3..841f1e9b7e37fbe2008ec992d264e84993c729be 100644 (file)
@@ -26,7 +26,14 @@ it('render issues list', () => {
   const issues = [{ key: 'foo' }, { key: 'bar' }];
   const onIssueClick = jest.fn();
   const wrapper = shallow(
-    <LineIssuesList issues={issues} line={line} onIssueClick={onIssueClick} selectedIssue="foo" />
+    <LineIssuesList
+      issues={issues}
+      line={line}
+      onIssueClick={onIssueClick}
+      onPopupToggle={jest.fn()}
+      openPopup={null}
+      selectedIssue="foo"
+    />
   );
   expect(wrapper).toMatchSnapshot();
 });
index 70d86769f06e75aa34fbaf89eec7044956120cc6..9f6da3b0b4be2108e231dec1c3da5f14776b32cb 100644 (file)
@@ -43,6 +43,8 @@ exports[`render code 1`] = `
       ]
     }
     onIssueClick={[Function]}
+    onPopupToggle={[Function]}
+    openPopup={null}
     selectedIssue="issue-1"
   />
 </td>
index 9008da985b07207b11b86938f65cc0589cf92a37..f31277211ba72aad9583ca248167796af4c7140d 100644 (file)
@@ -11,6 +11,8 @@ exports[`render issues list 1`] = `
       }
     }
     onClick={[Function]}
+    onPopupToggle={[Function]}
+    openPopup={null}
     selected={true}
   />
   <BaseIssue
@@ -20,6 +22,8 @@ exports[`render issues list 1`] = `
       }
     }
     onClick={[Function]}
+    onPopupToggle={[Function]}
+    openPopup={null}
     selected={false}
   />
 </div>
index 674d9c35216eb90064b954c69688ab812149be6d..70a1fc907462b14cd9515a1a0a0c3c0f90e8604b 100644 (file)
@@ -35,20 +35,14 @@ type Props = {|
   onCheck?: string => void,
   onClick: string => void,
   onFilter?: (property: string, issue: Issue) => void,
+  onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
+  openPopup: ?string,
   selected: boolean
 |};
 */
 
-/*::
-type State = {
-  currentPopup: string
-};
-*/
-
 export default class BaseIssue extends React.PureComponent {
-  /*:: mounted: boolean; */
   /*:: props: Props; */
-  /*:: state: State; */
 
   static contextTypes = {
     store: PropTypes.object
@@ -58,15 +52,7 @@ export default class BaseIssue extends React.PureComponent {
     selected: false
   };
 
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      currentPopup: ''
-    };
-  }
-
   componentDidMount() {
-    this.mounted = true;
     if (this.props.selected) {
       this.bindShortcuts();
     }
@@ -85,7 +71,6 @@ export default class BaseIssue extends React.PureComponent {
   }
 
   componentWillUnmount() {
-    this.mounted = false;
     if (this.props.selected) {
       this.unbindShortcuts();
     }
@@ -135,16 +120,7 @@ export default class BaseIssue extends React.PureComponent {
   }
 
   togglePopup = (popupName /*: string */, open /*: ?boolean */) => {
-    if (this.mounted) {
-      this.setState((prevState /*: State */) => {
-        if (prevState.currentPopup !== popupName && open !== false) {
-          return { currentPopup: popupName };
-        } else if (prevState.currentPopup === popupName && open !== true) {
-          return { currentPopup: '' };
-        }
-        return prevState;
-      });
-    }
+    this.props.onPopupToggle(this.props.issue.key, popupName, open);
   };
 
   handleAssignement = (login /*: string */) => {
@@ -175,7 +151,7 @@ export default class BaseIssue extends React.PureComponent {
         onFilter={this.props.onFilter}
         onChange={this.props.onChange}
         togglePopup={this.togglePopup}
-        currentPopup={this.state.currentPopup}
+        currentPopup={this.props.openPopup}
         selected={this.props.selected}
       />
     );
index be2f91af2ef0bf0bc2b4f3edd49974449d1fd68c..5800390c44ca9f1902d87913177c8004e6af896c 100644 (file)
@@ -30,7 +30,7 @@ import { deleteIssueComment, editIssueComment } from '../../api/issues';
 /*::
 type Props = {|
   checked?: boolean,
-  currentPopup: string,
+  currentPopup: ?string,
   issue: Issue,
   onAssign: string => void,
   onChange: Issue => void,
index 2c119cbb393230a63927f0d55d97e4e84cc97ef9..03837602471ef6d1d261fa48331490dff9d93775 100644 (file)
@@ -32,7 +32,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 /*::
 type Props = {
   issue: Issue,
-  currentPopup: string,
+  currentPopup: ?string,
   onAssign: string => void,
   onChange: Issue => void,
   onFail: Error => void,
index 718d456df06918d3504596e92e65afe7f17cee91..b756e185da96b65ae46707b67a21f57e83114eb2 100644 (file)
@@ -29,7 +29,7 @@ import { translate } from '../../../helpers/l10n';
 /*::
 type Props = {|
   commentPlaceholder: string,
-  currentPopup: string,
+  currentPopup: ?string,
   issueKey: string,
   onChange: Issue => void,
   onFail: Error => void,
index c6958f6a7687fec6daacf539110d23f88115186c..82aa5852ef4f9571cb479e261da9260fc51de21b 100644 (file)
@@ -34,7 +34,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 /*::
 type Props = {|
   issue: Issue,
-  currentPopup: string,
+  currentPopup: ?string,
   onFail: Error => void,
   onFilter?: (property: string, issue: Issue) => void,
   togglePopup: (string, boolean | void) => void
index c8ac8e0598672822905d4335514995805d39855c..4485f3edea09115be9e4ec017546e1ce6b6183e8 100644 (file)
@@ -26,7 +26,7 @@ it('should render correctly', () => {
   const element = shallow(
     <IssueCommentAction
       issueKey="issue-key"
-      currentPopup=""
+      currentPopup={null}
       onFail={jest.fn()}
       onIssueChange={jest.fn()}
       toggleComment={jest.fn()}
@@ -40,7 +40,7 @@ it('should open the popup when the button is clicked', () => {
   const element = shallow(
     <IssueCommentAction
       issueKey="issue-key"
-      currentPopup=""
+      currentPopup={null}
       onFail={jest.fn()}
       onIssueChange={jest.fn()}
       toggleComment={toggle}
index 407b05eb18958fc93ebbba3be7fb3b482b0c6e2a..007163c666632c8e24b3a9628de44fd4e1441352 100644 (file)
@@ -41,7 +41,7 @@ const issue = {
 
 it('should render the titlebar correctly', () => {
   const element = shallow(
-    <IssueTitleBar issue={issue} currentPopup="" onFail={jest.fn()} togglePopup={jest.fn()} />
+    <IssueTitleBar issue={issue} currentPopup={null} onFail={jest.fn()} togglePopup={jest.fn()} />
   );
   expect(element).toMatchSnapshot();
 });
@@ -50,7 +50,7 @@ it('should render the titlebar with the filter', () => {
   const element = shallow(
     <IssueTitleBar
       issue={issue}
-      currentPopup=""
+      currentPopup={null}
       onFail={jest.fn()}
       onFilter={jest.fn()}
       togglePopup={jest.fn()}