--- /dev/null
-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>
+ );
+ }
+ }
--- /dev/null
-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>
+ );
+ }
+ }
--- /dev/null
- 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;
+ }
--- /dev/null
-<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>
+ `;
</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 {
--- /dev/null
- 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" />;
+}