]> source.dussan.org Git - sonarqube.git/commitdiff
Merge branch 'branch-6.7'
authorStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Dec 2017 09:39:12 +0000 (10:39 +0100)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Dec 2017 09:39:12 +0000 (10:39 +0100)
12 files changed:
1  2 
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx
server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 0000000000000000000000000000000000000000,f05ba7eef691406bfb94e23340f1307e8aa98691..f5be8f05f056b7d3d03637e594ddf27a3a7a7ccf
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,153 +1,132 @@@
 -import { Link } from 'react-router';
+ /*
+  * 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';
 -  handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
 -    event.preventDefault();
 -    event.currentTarget.blur();
+ import RestoreAccessModal from './RestoreAccessModal';
+ import { Project } from './utils';
+ import { getComponentShow } from '../../api/components';
+ import { getComponentNavigation } from '../../api/nav';
++import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown';
+ 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();
+     }
+   };
 -  handleRestoreAccessClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
 -    event.preventDefault();
 -    event.currentTarget.blur();
++  handleApplyTemplateClick = () => {
+     this.props.onApplyTemplate(this.props.project);
+   };
 -    const { hasAccess, loading } = this.state;
++  handleRestoreAccessClick = () => {
+     this.setState({ restoreAccessModal: true });
+   };
+   handleRestoreAccessClose = () => this.setState({ restoreAccessModal: false });
+   handleRestoreAccessDone = () => {
+     this.setState({ hasAccess: true, restoreAccessModal: false });
+   };
+   render() {
 -      <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>
++    const { hasAccess } = this.state;
+     return (
 -      </div>
++      <ActionsDropdown key="dropdown" onToggleClick={this.handleDropdownClick}>
++        {hasAccess === true && (
++          <ActionsDropdownItem to={getComponentPermissionsUrl(this.props.project.key)}>
++            {translate('edit_permissions')}
++          </ActionsDropdownItem>
+         )}
++        {hasAccess === false && (
++          <ActionsDropdownItem
++            className="js-restore-access"
++            onClick={this.handleRestoreAccessClick}>
++            {translate('global_permissions.restore_access')}
++          </ActionsDropdownItem>
++        )}
++
++        <ActionsDropdownItem className="js-apply-template" onClick={this.handleApplyTemplateClick}>
++          {translate('projects_role.apply_template')}
++        </ActionsDropdownItem>
++
+         {this.state.restoreAccessModal && (
+           <RestoreAccessModal
+             currentUser={this.props.currentUser}
++            key="restore-access-modal"
+             onClose={this.handleRestoreAccessClose}
+             onRestoreAccess={this.handleRestoreAccessDone}
+             project={this.props.project}
+           />
+         )}
++      </ActionsDropdown>
+     );
+   }
+ }
index 0000000000000000000000000000000000000000,104c71864be55490035c63179d91f15dc51654cf..6b1958f6fc541a6d21f1e84af34933053cdd031d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,113 +1,110 @@@
 -import Modal from 'react-modal';
+ /*
+  * 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 { FormattedMessage } from 'react-intl';
++import { FormattedMessage } from 'react-intl';
+ import { Project } from './utils';
+ import { grantPermissionToUser } from '../../api/permissions';
++import Modal from '../../components/controls/Modal';
+ import { translate } from '../../helpers/l10n';
 -export default class BulkApplyTemplateModal extends React.PureComponent<Props, State> {
+ interface Props {
+   currentUser: { login: string };
+   onClose: () => void;
+   onRestoreAccess: () => void;
+   project: Project;
+ }
+ interface State {
+   loading: boolean;
+ }
 -      <Modal
 -        isOpen={true}
 -        contentLabel={header}
 -        className="modal"
 -        overlayClassName="modal-overlay"
 -        onRequestClose={this.props.onClose}>
 -        <header className="modal-head">
 -          <h2>{header}</h2>
 -        </header>
 -
++export default class RestoreAccessModal 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 (
 -            <button disabled={this.state.loading}>{translate('restore')}</button>
++      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+         <form onSubmit={this.handleFormSubmit}>
++          <header className="modal-head">
++            <h2>{header}</h2>
++          </header>
++
+           <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} type="submit">
++              {translate('restore')}
++            </button>
+             <a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
+               {translate('cancel')}
+             </a>
+           </footer>
+         </form>
+       </Modal>
+     );
+   }
+ }
index 0000000000000000000000000000000000000000,b6678d3e45e12e314a112174a4b3c77d4c88ab07..ac55dd97de9e9bb8b466fd3fcf86ff4cc735bbd7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,75 +1,75 @@@
 -  click(wrapper.find('.dropdown-toggle'));
+ /*
+  * 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();
++  wrapper.prop<Function>('onToggleClick')();
+   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 0000000000000000000000000000000000000000,b376ea3f6a69b722ff441540486504d2bd59b5fe..a3bae67199d7ae76cd9e640ba87598d6f4e06dc1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,131 +1,75 @@@
 -<div
 -  className="dropdown"
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
+ exports[`restores access 1`] = `
 -  <button
 -    className="dropdown-toggle"
 -    data-toggle="dropdown"
++<ActionsDropdown
++  key="dropdown"
++  onToggleClick={[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>
++  <ActionsDropdownItem
++    className="js-apply-template"
+     onClick={[Function]}
+   >
 -<div
 -  className="dropdown"
++    projects_role.apply_template
++  </ActionsDropdownItem>
++</ActionsDropdown>
+ `;
+ exports[`restores access 2`] = `
 -  <button
 -    className="dropdown-toggle"
 -    data-toggle="dropdown"
++<ActionsDropdown
++  key="dropdown"
++  onToggleClick={[Function]}
+ >
 -    actions
 -     
 -    <i
 -      className="icon-dropdown"
 -    />
 -  </button>
 -  <ul
 -    className="dropdown-menu dropdown-menu-right"
++  <ActionsDropdownItem
++    className="js-restore-access"
+     onClick={[Function]}
+   >
 -    <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>
++    global_permissions.restore_access
++  </ActionsDropdownItem>
++  <ActionsDropdownItem
++    className="js-apply-template"
++    onClick={[Function]}
+   >
 -<div
 -  className="dropdown"
++    projects_role.apply_template
++  </ActionsDropdownItem>
++</ActionsDropdown>
+ `;
+ exports[`restores access 3`] = `
 -  <button
 -    className="dropdown-toggle"
 -    data-toggle="dropdown"
++<ActionsDropdown
++  key="dropdown"
++  onToggleClick={[Function]}
+ >
 -    actions
 -     
 -    <i
 -      className="icon-dropdown"
 -    />
 -  </button>
 -  <ul
 -    className="dropdown-menu dropdown-menu-right"
++  <ActionsDropdownItem
++    className="js-restore-access"
+     onClick={[Function]}
+   >
 -    <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
++    global_permissions.restore_access
++  </ActionsDropdownItem>
++  <ActionsDropdownItem
++    className="js-apply-template"
++    onClick={[Function]}
+   >
 -</div>
++    projects_role.apply_template
++  </ActionsDropdownItem>
++  <RestoreAccessModal
+     currentUser={
+       Object {
+         "login": "admin",
+       }
+     }
++    key="restore-access-modal"
+     onClose={[Function]}
+     onRestoreAccess={[Function]}
+     project={
+       Object {
+         "id": "",
+         "key": "project",
+         "name": "Project",
+         "organization": "org",
+         "qualifier": "TRK",
+         "visibility": "private",
+       }
+     }
+   />
++</ActionsDropdown>
+ `;
index 6705b982c5aee5f20208d9a5d17f1e700d7698d7,7bae7a95d3e53aeec4adca378a4b135b1ada1df4..7a3728a28ad140d0e73a730153e143134c921b90
@@@ -25,8 -25,12 +25,13 @@@ exports[`renders list of projects 1`] 
    </thead>
    <tbody>
      <ProjectRow
-       onApplyTemplateClick={[Function]}
+       currentUser={
+         Object {
+           "login": "foo",
+         }
+       }
 +      key="a"
+       onApplyTemplate={[Function]}
        onProjectCheck={[Function]}
        project={
          Object {
        selected={true}
      />
      <ProjectRow
-       onApplyTemplateClick={[Function]}
+       currentUser={
+         Object {
+           "login": "foo",
+         }
+       }
 +      key="b"
+       onApplyTemplate={[Function]}
        onProjectCheck={[Function]}
        project={
          Object {
index a21b0f533a4f8d2868448d3dddecd0918f67365c,0000000000000000000000000000000000000000..0b4194d26d6c7809cb40838a7d70363a15122235
mode 100644,000000..100644
--- /dev/null
@@@ -1,115 -1,0 +1,118 @@@
-         data-toggle="dropdown">
 +/*
 + * 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 * as classNames from 'classnames';
 +import { Link } from 'react-router';
 +import { LocationDescriptor } from 'history';
 +import SettingsIcon from '../icons-components/SettingsIcon';
 +
 +interface Props {
 +  className?: string;
 +  children: React.ReactNode;
 +  menuClassName?: string;
 +  menuPosition?: 'left' | 'right';
++  // TODO: replace with `onOpen` & `onClose`
++  onToggleClick?: () => void;
 +  small?: boolean;
 +  toggleClassName?: string;
 +}
 +
 +export default function ActionsDropdown({ menuPosition = 'right', ...props }: Props) {
 +  return (
 +    <div className={classNames('dropdown', props.className)}>
 +      <button
 +        className={classNames('dropdown-toggle', props.toggleClassName, {
 +          'button-small': props.small
 +        })}
++        data-toggle="dropdown"
++        onClick={props.onToggleClick}>
 +        <SettingsIcon className="text-text-bottom" />
 +        <i className="icon-dropdown little-spacer-left" />
 +      </button>
 +      <ul
 +        className={classNames('dropdown-menu', props.menuClassName, {
 +          'dropdown-menu-right': menuPosition === 'right'
 +        })}>
 +        {props.children}
 +      </ul>
 +    </div>
 +  );
 +}
 +
 +interface ItemProps {
 +  className?: string;
 +  children: React.ReactNode;
 +  destructive?: boolean;
 +  /** used to pass a name of downloaded file */
 +  download?: string;
 +  id?: string;
 +  onClick?: () => void;
 +  to?: LocationDescriptor;
 +}
 +
 +export class ActionsDropdownItem extends React.PureComponent<ItemProps> {
 +  handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
 +    event.preventDefault();
 +    event.currentTarget.blur();
 +    if (this.props.onClick) {
 +      this.props.onClick();
 +    }
 +  };
 +
 +  render() {
 +    const className = classNames(this.props.className, { 'text-danger': this.props.destructive });
 +
 +    if (this.props.download && typeof this.props.to === 'string') {
 +      return (
 +        <li>
 +          <a
 +            className={className}
 +            download={this.props.download}
 +            href={this.props.to}
 +            id={this.props.id}>
 +            {this.props.children}
 +          </a>
 +        </li>
 +      );
 +    }
 +
 +    if (this.props.to) {
 +      return (
 +        <li>
 +          <Link className={className} id={this.props.id} to={this.props.to}>
 +            {this.props.children}
 +          </Link>
 +        </li>
 +      );
 +    }
 +
 +    return (
 +      <li>
 +        <a className={className} href="#" id={this.props.id} onClick={this.handleClick}>
 +          {this.props.children}
 +        </a>
 +      </li>
 +    );
 +  }
 +}
 +
 +export function ActionsDropdownDivider() {
 +  return <li className="divider" />;
 +}