]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10067 add "Restore Access" action on projects management page
authorStas Vilchik <stas.vilchik@sonarsource.com>
Thu, 7 Dec 2017 08:57:36 +0000 (09:57 +0100)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Dec 2017 08:41:15 +0000 (09:41 +0100)
14 files changed:
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index f92519a6a9e67df6f8b580d2ea476e8271732591..c714647d45abd92dedcf28126e729bdd5d1df590 100644 (file)
@@ -31,6 +31,7 @@ import { Organization } from '../../app/types';
 import { translate } from '../../helpers/l10n';
 
 export interface Props {
+  currentUser: { login: string };
   hasProvisionPermission?: boolean;
   onVisibilityChange: (visibility: string) => void;
   organization: Organization;
@@ -191,6 +192,7 @@ export default class App extends React.PureComponent<Props, State> {
         />
 
         <Projects
+          currentUser={this.props.currentUser}
           ready={this.state.ready}
           projects={this.state.projects}
           selection={this.state.selection}
index 0b00f3eba7652216d049d0001d4a81ff59050da6..199132d1a0b5ae5b90a495ef0cc37aa58f6ae140 100644 (file)
@@ -22,7 +22,7 @@ import { connect } from 'react-redux';
 import App from './App';
 import { Organization } from '../../app/types';
 import { onFail } from '../../store/rootActions';
-import { getAppState, getOrganizationByKey } from '../../store/rootReducer';
+import { getAppState, getOrganizationByKey, getCurrentUser } from '../../store/rootReducer';
 import { receiveOrganizations } from '../../store/organizations/duck';
 import { changeProjectVisibility } from '../../api/organizations';
 import { fetchOrganization } from '../../apps/organizations/actions';
@@ -32,6 +32,7 @@ interface Props {
     defaultOrganization: string;
     qualifiers: string[];
   };
+  currentUser: { login: string };
   fetchOrganization: (organization: string) => void;
   onVisibilityChange: (organization: Organization, visibility: string) => void;
   onRequestFail: (error: any) => void;
@@ -64,6 +65,7 @@ class AppContainer extends React.PureComponent<Props> {
 
     return (
       <App
+        currentUser={this.props.currentUser}
         hasProvisionPermission={organization.canProvisionProjects}
         onVisibilityChange={this.handleVisibilityChange}
         organization={organization}
@@ -75,6 +77,7 @@ class AppContainer extends React.PureComponent<Props> {
 
 const mapStateToProps = (state: any, ownProps: Props) => ({
   appState: getAppState(state),
+  currentUser: getCurrentUser(state),
   organization:
     ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization)
 });
index ce7df2a1c43a029f438ec69c5b8da5c9fdb2ccd4..9b243f7ec6b61cfa28f77bd9d81caeed99876f5f 100644 (file)
  */
 import * as React from 'react';
 import { Link } from 'react-router';
+import ProjectRowActions from './ProjectRowActions';
 import { Project } from './utils';
 import { Visibility } from '../../app/types';
 import PrivateBadge from '../../components/common/PrivateBadge';
 import Checkbox from '../../components/controls/Checkbox';
 import QualifierIcon from '../../components/shared/QualifierIcon';
-import { translate } from '../../helpers/l10n';
-import { getComponentPermissionsUrl } from '../../helpers/urls';
 import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
 
 interface Props {
-  onApplyTemplateClick: (project: Project) => void;
+  currentUser: { login: string };
+  onApplyTemplate: (project: Project) => void;
   onProjectCheck: (project: Project, checked: boolean) => void;
   project: Project;
   selected: boolean;
@@ -40,12 +40,6 @@ export default class ProjectRow extends React.PureComponent<Props> {
     this.props.onProjectCheck(this.props.project, checked);
   };
 
-  handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.props.onApplyTemplateClick(this.props.project);
-  };
-
   render() {
     const { project, selected } = this.props;
 
@@ -82,23 +76,11 @@ export default class ProjectRow extends React.PureComponent<Props> {
         </td>
 
         <td className="thin nowrap">
-          <div className="dropdown">
-            <button className="dropdown-toggle" data-toggle="dropdown">
-              {translate('actions')} <i className="icon-dropdown" />
-            </button>
-            <ul className="dropdown-menu dropdown-menu-right">
-              <li>
-                <Link to={getComponentPermissionsUrl(project.key)}>
-                  {translate('edit_permissions')}
-                </Link>
-              </li>
-              <li>
-                <a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}>
-                  {translate('projects_role.apply_template')}
-                </a>
-              </li>
-            </ul>
-          </div>
+          <ProjectRowActions
+            currentUser={this.props.currentUser}
+            onApplyTemplate={this.props.onApplyTemplate}
+            project={project}
+          />
         </td>
       </tr>
     );
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
new file mode 100644 (file)
index 0000000..f05ba7e
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import RestoreAccessModal from './RestoreAccessModal';
+import { Project } from './utils';
+import { getComponentShow } from '../../api/components';
+import { getComponentNavigation } from '../../api/nav';
+import { translate } from '../../helpers/l10n';
+import { getComponentPermissionsUrl } from '../../helpers/urls';
+
+export interface Props {
+  currentUser: { login: string };
+  onApplyTemplate: (project: Project) => void;
+  project: Project;
+}
+
+interface State {
+  hasAccess?: boolean;
+  loading: boolean;
+  restoreAccessModal: boolean;
+}
+
+export default class ProjectRowActions extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { loading: false, restoreAccessModal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchPermissions = () => {
+    this.setState({ loading: false });
+    // call `getComponentNavigation` to check if user has the "Administer" permission
+    // call `getComponentShow` to check if user has the "Browse" permission
+    Promise.all([
+      getComponentNavigation(this.props.project.key),
+      getComponentShow(this.props.project.key)
+    ]).then(
+      ([navResponse]) => {
+        if (this.mounted) {
+          const hasAccess = Boolean(
+            navResponse.configuration && navResponse.configuration.showPermissions
+          );
+          this.setState({ hasAccess, loading: false });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ hasAccess: false, loading: false });
+        }
+      }
+    );
+  };
+
+  handleDropdownClick = () => {
+    if (this.state.hasAccess === undefined && !this.state.loading) {
+      this.fetchPermissions();
+    }
+  };
+
+  handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onApplyTemplate(this.props.project);
+  };
+
+  handleRestoreAccessClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.setState({ restoreAccessModal: true });
+  };
+
+  handleRestoreAccessClose = () => this.setState({ restoreAccessModal: false });
+
+  handleRestoreAccessDone = () => {
+    this.setState({ hasAccess: true, restoreAccessModal: false });
+  };
+
+  render() {
+    const { hasAccess, loading } = this.state;
+
+    return (
+      <div className="dropdown">
+        <button
+          className="dropdown-toggle"
+          data-toggle="dropdown"
+          onClick={this.handleDropdownClick}>
+          {translate('actions')} <i className="icon-dropdown" />
+        </button>
+        {loading ? (
+          <div className="dropdown-menu dropdown-menu-right">
+            <i className="spinner spacer-left" />
+          </div>
+        ) : (
+          <ul className="dropdown-menu dropdown-menu-right">
+            {hasAccess === true && (
+              <li>
+                <Link to={getComponentPermissionsUrl(this.props.project.key)}>
+                  {translate('edit_permissions')}
+                </Link>
+              </li>
+            )}
+
+            {hasAccess === false && (
+              <li>
+                <a className="js-restore-access" href="#" onClick={this.handleRestoreAccessClick}>
+                  {translate('global_permissions.restore_access')}
+                </a>
+              </li>
+            )}
+
+            <li>
+              <a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}>
+                {translate('projects_role.apply_template')}
+              </a>
+            </li>
+          </ul>
+        )}
+
+        {this.state.restoreAccessModal && (
+          <RestoreAccessModal
+            currentUser={this.props.currentUser}
+            onClose={this.handleRestoreAccessClose}
+            onRestoreAccess={this.handleRestoreAccessDone}
+            project={this.props.project}
+          />
+        )}
+      </div>
+    );
+  }
+}
index 2a2b3c1cb2cc746ea793abce8f67ad33da390f1a..d33996850666f756e9b2d659ff8497a8357b37f2 100644 (file)
@@ -23,8 +23,10 @@ import ProjectRow from './ProjectRow';
 import { Project } from './utils';
 import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
 import { Organization } from '../../app/types';
+import { translate } from '../../helpers/l10n';
 
 interface Props {
+  currentUser: { login: string };
   onProjectDeselected: (project: string) => void;
   onProjectSelected: (project: string) => void;
   organization: Organization;
@@ -42,7 +44,7 @@ export default class Projects extends React.PureComponent<Props> {
     }
   };
 
-  onApplyTemplateClick = (project: Project) => {
+  handleApplyTemplate = (project: Project) => {
     new ApplyTemplateView({ project, organization: this.props.organization }).render();
   };
 
@@ -54,18 +56,19 @@ export default class Projects extends React.PureComponent<Props> {
         <thead>
           <tr>
             <th />
-            <th>Name</th>
+            <th>{translate('name')}</th>
             <th />
-            <th>Key</th>
-            <th className="thin nowrap text-right">Last Analysis</th>
+            <th>{translate('key')}</th>
+            <th className="thin nowrap text-right">{translate('last_analysis')}</th>
             <th />
           </tr>
         </thead>
         <tbody>
           {this.props.projects.map(project => (
             <ProjectRow
+              currentUser={this.props.currentUser}
               key={project.key}
-              onApplyTemplateClick={this.onApplyTemplateClick}
+              onApplyTemplate={this.handleApplyTemplate}
               onProjectCheck={this.onProjectCheck}
               project={project}
               selected={this.props.selection.includes(project.key)}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
new file mode 100644 (file)
index 0000000..104c718
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Modal from 'react-modal';
+import { Project } from './utils';
+import { grantPermissionToUser } from '../../api/permissions';
+import { translate } from '../../helpers/l10n';
+import { FormattedMessage } from 'react-intl';
+
+interface Props {
+  currentUser: { login: string };
+  onClose: () => void;
+  onRestoreAccess: () => void;
+  project: Project;
+}
+
+interface State {
+  loading: boolean;
+}
+
+export default class BulkApplyTemplateModal extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { loading: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.props.onClose();
+  };
+
+  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    this.setState({ loading: true });
+    Promise.all([this.grantPermission('user'), this.grantPermission('admin')]).then(
+      this.props.onRestoreAccess,
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  grantPermission = (permission: string) =>
+    grantPermissionToUser(
+      this.props.project.key,
+      this.props.currentUser.login,
+      permission,
+      this.props.project.organization
+    );
+
+  render() {
+    const header = translate('global_permissions.restore_access');
+
+    return (
+      <Modal
+        isOpen={true}
+        contentLabel={header}
+        className="modal"
+        overlayClassName="modal-overlay"
+        onRequestClose={this.props.onClose}>
+        <header className="modal-head">
+          <h2>{header}</h2>
+        </header>
+
+        <form onSubmit={this.handleFormSubmit}>
+          <div className="modal-body">
+            <FormattedMessage
+              defaultMessage={translate('global_permissions.restore_access.message')}
+              id="global_permissions.restore_access.message"
+              values={{
+                browse: <strong>{translate('projects_role.user')}</strong>,
+                administer: <strong>{translate('projects_role.admin')}</strong>
+              }}
+            />
+          </div>
+
+          <footer className="modal-foot">
+            {this.state.loading && <i className="spinner spacer-right" />}
+            <button disabled={this.state.loading}>{translate('restore')}</button>
+            <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
+              {translate('cancel')}
+            </a>
+          </footer>
+        </form>
+      </Modal>
+    );
+  }
+}
index 0fb6b1d21d057e220584a5425b97a3da13f6e644..36dde628a47f007f5201df7538fdebe70e2e6120 100644 (file)
@@ -138,6 +138,7 @@ it('changes default project visibility', () => {
 function mountRender(props?: { [P in keyof Props]?: Props[P] }) {
   return mount(
     <App
+      currentUser={{ login: 'foo' }}
       hasProvisionPermission={true}
       onVisibilityChange={jest.fn()}
       organization={organization}
index b1d052dbb86e347a94ba14489946e2ec357a1236..20e180df5cd25eb03500066f5937c4d5b255d01e 100644 (file)
@@ -21,7 +21,6 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import ProjectRow from '../ProjectRow';
 import { Visibility } from '../../../app/types';
-import { click } from '../../../helpers/testUtils';
 
 const project = {
   key: 'project',
@@ -44,17 +43,11 @@ it('checks project', () => {
   expect(onProjectCheck).toBeCalledWith(project, false);
 });
 
-it('applies permission template', () => {
-  const onApplyTemplateClick = jest.fn();
-  const wrapper = shallowRender({ onApplyTemplateClick });
-  click(wrapper.find('.js-apply-template'));
-  expect(onApplyTemplateClick).toBeCalledWith(project);
-});
-
 function shallowRender(props?: any) {
   return shallow(
     <ProjectRow
-      onApplyTemplateClick={jest.fn()}
+      currentUser={{ login: 'foo' }}
+      onApplyTemplate={jest.fn()}
       onProjectCheck={jest.fn()}
       project={project}
       selected={true}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
new file mode 100644 (file)
index 0000000..b6678d3
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ProjectRowActions, { Props } from '../ProjectRowActions';
+import { Visibility } from '../../../app/types';
+import { click } from '../../../helpers/testUtils';
+
+jest.mock('../../../api/components', () => ({
+  getComponentShow: jest.fn(() => Promise.reject(undefined))
+}));
+
+jest.mock('../../../api/nav', () => ({
+  getComponentNavigation: jest.fn(() => Promise.resolve())
+}));
+
+const project = {
+  id: '',
+  key: 'project',
+  name: 'Project',
+  organization: 'org',
+  qualifier: 'TRK',
+  visibility: Visibility.Private
+};
+
+it('restores access', async () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('.dropdown-toggle'));
+  await new Promise(setImmediate);
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('.js-restore-access'));
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('applies permission template', () => {
+  const onApplyTemplate = jest.fn();
+  const wrapper = shallowRender({ onApplyTemplate });
+  click(wrapper.find('.js-apply-template'));
+  expect(onApplyTemplate).toBeCalledWith(project);
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  const wrapper = shallow(
+    <ProjectRowActions
+      currentUser={{ login: 'admin' }}
+      onApplyTemplate={jest.fn()}
+      project={project}
+      {...props}
+    />
+  );
+  (wrapper.instance() as ProjectRowActions).mounted = true;
+  return wrapper;
+}
index 792e29ccc486f9fddb612ad0832da3bdd9c2d02b..23f93bc8d26d1214150f3ac6e9de793b02c3f290 100644 (file)
@@ -59,13 +59,14 @@ it('opens modal to apply permission template', () => {
   wrapper
     .find('ProjectRow')
     .first()
-    .prop<Function>('onApplyTemplateClick')(projects[0]);
+    .prop<Function>('onApplyTemplate')(projects[0]);
   expect(ApplyTemplateView).toBeCalledWith({ organization, project: projects[0] });
 });
 
 function shallowRender(props?: any) {
   return shallow(
     <Projects
+      currentUser={{ login: 'foo' }}
       onProjectDeselected={jest.fn()}
       onProjectSelected={jest.fn()}
       organization={organization}
index 87d61db023e53234d6f4ccbd4417bdedb9561af0..49178bc7abdedf3bc3de0ace1b21a1d43562448f 100644 (file)
@@ -64,49 +64,22 @@ exports[`renders 1`] = `
   <td
     className="thin nowrap"
   >
-    <div
-      className="dropdown"
-    >
-      <button
-        className="dropdown-toggle"
-        data-toggle="dropdown"
-      >
-        actions
-         
-        <i
-          className="icon-dropdown"
-        />
-      </button>
-      <ul
-        className="dropdown-menu dropdown-menu-right"
-      >
-        <li>
-          <Link
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to={
-              Object {
-                "pathname": "/project_roles",
-                "query": Object {
-                  "id": "project",
-                },
-              }
-            }
-          >
-            edit_permissions
-          </Link>
-        </li>
-        <li>
-          <a
-            className="js-apply-template"
-            href="#"
-            onClick={[Function]}
-          >
-            projects_role.apply_template
-          </a>
-        </li>
-      </ul>
-    </div>
+    <ProjectRowActions
+      currentUser={
+        Object {
+          "login": "foo",
+        }
+      }
+      onApplyTemplate={[Function]}
+      project={
+        Object {
+          "key": "project",
+          "name": "Project",
+          "qualifier": "TRK",
+          "visibility": "private",
+        }
+      }
+    />
   </td>
 </tr>
 `;
@@ -173,49 +146,23 @@ exports[`renders 2`] = `
   <td
     className="thin nowrap"
   >
-    <div
-      className="dropdown"
-    >
-      <button
-        className="dropdown-toggle"
-        data-toggle="dropdown"
-      >
-        actions
-         
-        <i
-          className="icon-dropdown"
-        />
-      </button>
-      <ul
-        className="dropdown-menu dropdown-menu-right"
-      >
-        <li>
-          <Link
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to={
-              Object {
-                "pathname": "/project_roles",
-                "query": Object {
-                  "id": "project",
-                },
-              }
-            }
-          >
-            edit_permissions
-          </Link>
-        </li>
-        <li>
-          <a
-            className="js-apply-template"
-            href="#"
-            onClick={[Function]}
-          >
-            projects_role.apply_template
-          </a>
-        </li>
-      </ul>
-    </div>
+    <ProjectRowActions
+      currentUser={
+        Object {
+          "login": "foo",
+        }
+      }
+      onApplyTemplate={[Function]}
+      project={
+        Object {
+          "key": "project",
+          "lastAnalysisDate": "2017-04-08T00:00:00.000Z",
+          "name": "Project",
+          "qualifier": "TRK",
+          "visibility": "private",
+        }
+      }
+    />
   </td>
 </tr>
 `;
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap
new file mode 100644 (file)
index 0000000..b376ea3
--- /dev/null
@@ -0,0 +1,131 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`restores access 1`] = `
+<div
+  className="dropdown"
+>
+  <button
+    className="dropdown-toggle"
+    data-toggle="dropdown"
+    onClick={[Function]}
+  >
+    actions
+     
+    <i
+      className="icon-dropdown"
+    />
+  </button>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li>
+      <a
+        className="js-apply-template"
+        href="#"
+        onClick={[Function]}
+      >
+        projects_role.apply_template
+      </a>
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`restores access 2`] = `
+<div
+  className="dropdown"
+>
+  <button
+    className="dropdown-toggle"
+    data-toggle="dropdown"
+    onClick={[Function]}
+  >
+    actions
+     
+    <i
+      className="icon-dropdown"
+    />
+  </button>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li>
+      <a
+        className="js-restore-access"
+        href="#"
+        onClick={[Function]}
+      >
+        global_permissions.restore_access
+      </a>
+    </li>
+    <li>
+      <a
+        className="js-apply-template"
+        href="#"
+        onClick={[Function]}
+      >
+        projects_role.apply_template
+      </a>
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`restores access 3`] = `
+<div
+  className="dropdown"
+>
+  <button
+    className="dropdown-toggle"
+    data-toggle="dropdown"
+    onClick={[Function]}
+  >
+    actions
+     
+    <i
+      className="icon-dropdown"
+    />
+  </button>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li>
+      <a
+        className="js-restore-access"
+        href="#"
+        onClick={[Function]}
+      >
+        global_permissions.restore_access
+      </a>
+    </li>
+    <li>
+      <a
+        className="js-apply-template"
+        href="#"
+        onClick={[Function]}
+      >
+        projects_role.apply_template
+      </a>
+    </li>
+  </ul>
+  <BulkApplyTemplateModal
+    currentUser={
+      Object {
+        "login": "admin",
+      }
+    }
+    onClose={[Function]}
+    onRestoreAccess={[Function]}
+    project={
+      Object {
+        "id": "",
+        "key": "project",
+        "name": "Project",
+        "organization": "org",
+        "qualifier": "TRK",
+        "visibility": "private",
+      }
+    }
+  />
+</div>
+`;
index 2c60880eb28eb6f42a10757619b739b7651e8fab..7bae7a95d3e53aeec4adca378a4b135b1ada1df4 100644 (file)
@@ -9,23 +9,28 @@ exports[`renders list of projects 1`] = `
     <tr>
       <th />
       <th>
-        Name
+        name
       </th>
       <th />
       <th>
-        Key
+        key
       </th>
       <th
         className="thin nowrap text-right"
       >
-        Last Analysis
+        last_analysis
       </th>
       <th />
     </tr>
   </thead>
   <tbody>
     <ProjectRow
-      onApplyTemplateClick={[Function]}
+      currentUser={
+        Object {
+          "login": "foo",
+        }
+      }
+      onApplyTemplate={[Function]}
       onProjectCheck={[Function]}
       project={
         Object {
@@ -38,7 +43,12 @@ exports[`renders list of projects 1`] = `
       selected={true}
     />
     <ProjectRow
-      onApplyTemplateClick={[Function]}
+      currentUser={
+        Object {
+          "login": "foo",
+        }
+      }
+      onApplyTemplate={[Function]}
       onProjectCheck={[Function]}
       project={
         Object {
index ae9235775a001ea48e705c2acb10b73d27105333..de2a97124084741ef6e90ee75c4145693e5051d7 100644 (file)
@@ -76,6 +76,7 @@ issues=Issues
 inheritance=Inheritance
 key=Key
 language=Language
+last_analysis=Last Analysis
 learn_more=Learn More
 library=Library
 line_number=Line Number
@@ -1891,6 +1892,8 @@ global_permissions.scan.desc=Ability to get all settings required to perform an
 global_permissions.provisioning=Create Projects
 global_permissions.provisioning.desc=Ability to initialize a project so its settings can be configured before the first analysis.
 global_permissions.filter_by_x_permission=Filter by "{0}" permission
+global_permissions.restore_access=Restore Access
+global_permissions.restore_access.message=You will receive {browse} and {administer} permissions on the project. Do you want to continue?