]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8669 Display organizations on the "Notifications" page (#1633)
authorStas Vilchik <stas-vilchik@users.noreply.github.com>
Tue, 7 Feb 2017 12:11:07 +0000 (13:11 +0100)
committerGitHub <noreply@github.com>
Tue, 7 Feb 2017 12:11:07 +0000 (13:11 +0100)
server/sonar-web/src/main/js/api/notifications.js
server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js
server/sonar-web/src/main/js/apps/account/notifications/Projects.js
server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap
server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.js.snap
server/sonar-web/src/main/js/apps/account/notifications/actions.js
server/sonar-web/src/main/js/components/shared/Organization.js
server/sonar-web/src/main/js/helpers/urls.js
server/sonar-web/src/main/js/store/notifications/duck.js

index 4ca5b24dd65477a3199edefcfc85ff7ab9215fce..ec433d1e1b34b139e76204cd6682d411a247eea0 100644 (file)
@@ -24,8 +24,9 @@ export type GetNotificationsResponse = {
   notifications: Array<{
     channel: string,
     type: string,
-    project: string | null,
-    projectName: string | null
+    organization?: string,
+    project?: string,
+    projectName?: string
   }>,
   channels: Array<string>,
   globalTypes: Array<string>,
@@ -36,7 +37,7 @@ export const getNotifications = (): Promise<GetNotificationsResponse> => (
     getJSON('/api/notifications/list')
 );
 
-export const addNotification = (channel: string, type: string, project: string | null): Promise<*> => {
+export const addNotification = (channel: string, type: string, project?: string): Promise<*> => {
   const data: Object = { channel, type };
   if (project) {
     Object.assign(data, { project });
@@ -44,7 +45,7 @@ export const addNotification = (channel: string, type: string, project: string |
   return post('/api/notifications/add', data);
 };
 
-export const removeNotification = (channel: string, type: string, project: string | null): Promise<*> => {
+export const removeNotification = (channel: string, type: string, project?: string): Promise<*> => {
   const data: Object = { channel, type };
   if (project) {
     Object.assign(data, { project });
index 828e4ec3ebad64e8e648b9246a7550afda9d323d..55279c4ed96c5b52cb04e4f4f16f620acc330db3 100644 (file)
  */
 import React from 'react';
 import { connect } from 'react-redux';
+import { Link } from 'react-router';
 import NotificationsList from './NotificationsList';
+import Organization from '../../../components/shared/Organization';
 import { translate } from '../../../helpers/l10n';
 import {
-  getProjectNotifications,
-  getNotificationChannels,
-  getNotificationPerProjectTypes
+getProjectNotifications,
+getNotificationChannels,
+getNotificationPerProjectTypes
 } from '../../../store/rootReducer';
 import type {
-  Notification,
-  NotificationsState,
-  ChannelsState,
-  TypesState
+Notification,
+NotificationsState,
+ChannelsState,
+TypesState
 } from '../../../store/notifications/duck';
 import { addNotification, removeNotification } from './actions';
+import { getProjectUrl } from '../../../helpers/urls';
 
 class ProjectNotifications extends React.Component {
   props: {
@@ -72,7 +75,10 @@ class ProjectNotifications extends React.Component {
           <thead>
             <tr>
               <th>
-                <h4 className="display-inline-block">{project.name}</h4>
+                <span className="text-normal"><Organization organizationKey={project.organization}/></span>
+                <h4 className="display-inline-block">
+                  <Link to={getProjectUrl(project.key)}>{project.name}</Link>
+                </h4>
               </th>
               {channels.map(channel => (
                   <th key={channel} className="text-center">
index b9c189b4002d329965fb8f1b6d9e6a01ed7d0efc..72153b35b2986a146407c2e8b7ee97d33d996fa6 100644 (file)
@@ -22,8 +22,9 @@ import Select from 'react-select';
 import { connect } from 'react-redux';
 import differenceBy from 'lodash/differenceBy';
 import ProjectNotifications from './ProjectNotifications';
+import Organization from '../../../components/shared/Organization';
 import { translate } from '../../../helpers/l10n';
-import { getComponents } from '../../../api/components';
+import { getSuggestions } from '../../../api/components';
 import { getProjectsWithNotifications } from '../../../store/rootReducer';
 
 type Props = {
@@ -60,19 +61,38 @@ class Projects extends React.Component {
     }
   }
 
-  loadOptions = query => {
-    // TODO filter existing out
-    return getComponents({ qualifiers: 'TRK', q: query })
-        .then(r => r.components)
+  renderOption = option => {
+    return (
+        <span>
+          <Organization organizationKey={option.organization} link={false}/>
+          <strong>{option.label}</strong>
+        </span>
+    );
+  }
+
+  loadOptions = (query, cb) => {
+    if (query.length < 2) {
+      cb(null, { options: [] });
+      return;
+    }
+
+    getSuggestions(query)
+        .then(r => {
+          const projects = r.results.find(domain => domain.q === 'TRK');
+          return projects ? projects.items : [];
+        })
         .then(projects => projects.map(project => ({
           value: project.key,
-          label: project.name
+          label: project.name,
+          organization: project.organization
         })))
-        .then(options => ({ options }));
+        .then(options => {
+          cb(null, { options });
+        });
   };
 
   handleAddProject = selected => {
-    const project = { key: selected.value, name: selected.label };
+    const project = { key: selected.value, name: selected.label, organization: selected.organization };
     this.setState({
       addedProjects: [...this.state.addedProjects, project]
     });
@@ -102,13 +122,15 @@ class Projects extends React.Component {
               Set notifications for:
             </span>
             <Select.Async
+                autoload={false}
+                cache={false}
                 name="new_project"
                 style={{ width: '300px' }}
                 loadOptions={this.loadOptions}
                 minimumInput={2}
+                optionRenderer={this.renderOption}
                 onChange={this.handleAddProject}
-                placeholder="Search Project"
-                searchPromptText="Type at least 2 characters to search"/>
+                placeholder="Search Project"/>
           </div>
         </section>
     );
index 44df6e92f1af3644c01c77b01977dbae0382e41f..13344ac72aece55d4ff486db97180ec66d78a667 100644 (file)
@@ -4,9 +4,25 @@ exports[`test should match snapshot 1`] = `
   <thead>
     <tr>
       <th>
+        <span
+          className="text-normal">
+          <Connect(Organization) />
+        </span>
         <h4
           className="display-inline-block">
-          Foo
+          <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/dashboard",
+                "query": Object {
+                  "id": "foo",
+                },
+              }
+            }>
+            Foo
+          </Link>
         </h4>
       </th>
       <th
index 3c6d90e870bc1e0fe6c58771d417299889b45e0a..c1108cb2f21da627cd2d2166dca74fa97d1f7335 100644 (file)
@@ -25,8 +25,8 @@ exports[`test should render projects 1`] = `
       Set notifications for:
     </span>
     <Async
-      autoload={true}
-      cache={Object {}}
+      autoload={false}
+      cache={false}
       ignoreAccents={true}
       ignoreCase={true}
       loadOptions={[Function]}
@@ -34,9 +34,10 @@ exports[`test should render projects 1`] = `
       minimumInput={2}
       name="new_project"
       onChange={[Function]}
+      optionRenderer={[Function]}
       options={Array []}
       placeholder="Search Project"
-      searchPromptText="Type at least 2 characters to search"
+      searchPromptText="Type to search"
       style={
         Object {
           "width": "300px",
@@ -80,8 +81,8 @@ exports[`test should render projects 2`] = `
       Set notifications for:
     </span>
     <Async
-      autoload={true}
-      cache={Object {}}
+      autoload={false}
+      cache={false}
       ignoreAccents={true}
       ignoreCase={true}
       loadOptions={[Function]}
@@ -89,9 +90,10 @@ exports[`test should render projects 2`] = `
       minimumInput={2}
       name="new_project"
       onChange={[Function]}
+      optionRenderer={[Function]}
       options={Array []}
       placeholder="Search Project"
-      searchPromptText="Type at least 2 characters to search"
+      searchPromptText="Type to search"
       style={
         Object {
           "width": "300px",
@@ -135,8 +137,8 @@ exports[`test should render projects 3`] = `
       Set notifications for:
     </span>
     <Async
-      autoload={true}
-      cache={Object {}}
+      autoload={false}
+      cache={false}
       ignoreAccents={true}
       ignoreCase={true}
       loadOptions={[Function]}
@@ -144,9 +146,10 @@ exports[`test should render projects 3`] = `
       minimumInput={2}
       name="new_project"
       onChange={[Function]}
+      optionRenderer={[Function]}
       options={Array []}
       placeholder="Search Project"
-      searchPromptText="Type at least 2 characters to search"
+      searchPromptText="Type to search"
       style={
         Object {
           "width": "300px",
index 32d10641df4ec3a464860c21e9721fac49733528..c30af822106f5cb75a8f2ab3b162eaa550f57cc8 100644 (file)
@@ -20,7 +20,7 @@
 // @flow
 import * as api from '../../../api/notifications';
 import type { GetNotificationsResponse } from '../../../api/notifications';
-import { onFail } from '../../../store/rootActions';
+import { onFail, fetchOrganizations } from '../../../store/rootActions';
 import {
   receiveNotifications,
   addNotification as addNotificationAction,
@@ -30,12 +30,18 @@ import type { Notification } from '../../../store/notifications/duck';
 
 export const fetchNotifications = () => (dispatch: Function) => {
   const onFulfil = (response: GetNotificationsResponse) => {
-    dispatch(receiveNotifications(
-        response.notifications,
-        response.channels,
-        response.globalTypes,
-        response.perProjectTypes
-    ));
+    const organizations = response.notifications
+        .filter(n => n.organization)
+        .map(n => n.organization);
+
+    dispatch(fetchOrganizations(organizations)).then(() => {
+      dispatch(receiveNotifications(
+          response.notifications,
+          response.channels,
+          response.globalTypes,
+          response.perProjectTypes
+      ));
+    });
   };
 
   return api.getNotifications().then(onFulfil, onFail(dispatch));
index f803359455e85f7919c77089bda2462378792dd1..4835871864beebe56f912459b30c98947afb50f3 100644 (file)
@@ -28,17 +28,22 @@ type OwnProps = {
 };
 
 type Props = {
+  link?: boolean,
   organizationKey: string,
   organization: null | {
     key: string,
     name: string
   },
-  shouldBeDisplayed: boolean
+  shouldBeDisplayed: boolean,
 };
 
 class Organization extends React.Component {
   props: Props;
 
+  static defaultProps = {
+    link: true
+  };
+
   render () {
     const { organization, shouldBeDisplayed } = this.props;
 
@@ -48,7 +53,11 @@ class Organization extends React.Component {
 
     return (
         <span>
-          <OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
+          {this.props.link ? (
+              <OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
+          ) : (
+              organization.name
+          )}
           <span className="slash-separator"/>
         </span>
     );
index 6dddb630c09a2b178e19b9c1d923f33a094ae183..42416ae5cf4c959eb4ccae1f7c1871ef5dd286e9 100644 (file)
@@ -26,6 +26,13 @@ export function getComponentUrl (componentKey) {
   return window.baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey);
 }
 
+export function getProjectUrl (key) {
+  return {
+    pathname: '/dashboard',
+    query: { id: key }
+  };
+}
+
 /**
  * Generate URL for a global issues page
  * @param {object} query
index b53948850f9a3bff30b011e490270a440aa5a8d6..e5e6a8d4520ef1d24915795fee430d923790b2e0 100644 (file)
@@ -25,8 +25,9 @@ import uniqWith from 'lodash/uniqWith';
 export type Notification = {
   channel: string,
   type: string,
-  project: string | null,
-  projectName: string | null
+  project?: string,
+  projectName?: string,
+  organization?: string
 };
 
 export type NotificationsState = Array<Notification>;
@@ -147,7 +148,11 @@ export const getGlobal = (state: State): NotificationsState => (
 
 export const getProjects = (state: State): Array<string> => (
     uniqBy(
-        state.notifications.filter(n => n.project).map(n => ({ key: n.project, name: n.projectName })),
+        state.notifications.filter(n => n.project).map(n => ({
+          key: n.project,
+          name: n.projectName,
+          organization: n.organization
+        })),
         project => project.key
     )
 );